diff --git a/.eslintrc.js b/.eslintrc.js
index 08bc83d5b4..973bf18e97 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -52,7 +52,6 @@ module.exports = {
},
],
'sort-keys': 'off',
- 'import/no-default-export': 'error',
'import/no-unresolved': 'error',
'no-irregular-whitespace': 'error',
'no-unused-expressions': 'error',
@@ -78,7 +77,7 @@ module.exports = {
},
overrides: [
{
- files: ['*.js'],
+ files: ['*.js', '*test.ts'],
rules: {
'@typescript-eslint/no-var-requires': 0,
},
diff --git a/.playground/playgroud.tsx b/.playground/playgroud.tsx
index ea6b06dcbe..98c31be60b 100644
--- a/.playground/playgroud.tsx
+++ b/.playground/playgroud.tsx
@@ -1,38 +1,66 @@
import React from 'react';
-import {
- Axis,
- Chart,
- getAxisId,
- getSpecId,
- Position,
- ScaleType,
- HistogramBarSeries,
- DARK_THEME,
- Settings,
-} from '../src';
-import { KIBANA_METRICS } from '../src/utils/data_samples/test_dataset_kibana';
+import { Axis, Chart, getAxisId, getSpecId, Position, ScaleType, Settings, LineSeries } from '../src';
+import { Fit } from '../src/chart_types/xy_chart/utils/specs';
+
+const data = [
+ { x: 0, y: null },
+ { x: 1, y: 3 },
+ { x: 2, y: 5 },
+ { x: 3, y: null },
+ { x: 4, y: 4 },
+ { x: 5, y: null },
+ { x: 6, y: 5 },
+ { x: 7, y: 6 },
+ { x: 8, y: null },
+ { x: 9, y: null },
+ { x: 10, y: null },
+ { x: 11, y: 12 },
+ { x: 12, y: null },
+];
export class Playground extends React.Component {
render() {
- const data = KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 5);
return (
-
+ <>
+
+ >
);
}
}
diff --git a/global.d.ts b/global.d.ts
new file mode 100644
index 0000000000..0529f84411
--- /dev/null
+++ b/global.d.ts
@@ -0,0 +1,2 @@
+// https://github.com/jest-community/jest-extended
+import 'jest-extended';
diff --git a/integration/helpers.ts b/integration/helpers.ts
index 04f4b627bc..128326a1f4 100644
--- a/integration/helpers.ts
+++ b/integration/helpers.ts
@@ -24,6 +24,7 @@ function requireAllStories() {
function encodeString(string: string) {
return string
.replace(/\//gi, ' ')
+ .replace(/-/g, ' ')
.replace(/[^a-z|A-Z|0-9|\s|\/]+/gi, '')
.trim()
.replace(/\s+/g, '-')
diff --git a/integration/page_objects/common.ts b/integration/page_objects/common.ts
index 2178a2eba8..2a0bd73307 100644
--- a/integration/page_objects/common.ts
+++ b/integration/page_objects/common.ts
@@ -107,7 +107,7 @@ class CommonPage {
expect(chart).toMatchImageSnapshot();
} catch (error) {
- throw new Error(error);
+ throw new Error(`${error}\n\n${url}`);
}
}
async loadChartFromURL(url: string) {
diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-fitting-functions-non-stacked-series-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-fitting-functions-non-stacked-series-visually-looks-correct-1-snap.png
new file mode 100644
index 0000000000..5ee342375a
Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-fitting-functions-non-stacked-series-visually-looks-correct-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-same-naming-visually-looks-correct-2-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-same-naming-visually-looks-correct-2-snap.png
new file mode 100644
index 0000000000..ec4a40e69e
Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-same-naming-visually-looks-correct-2-snap.png differ
diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-mixed-charts-fitting-functions-non-stacked-series-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-mixed-charts-fitting-functions-non-stacked-series-visually-looks-correct-1-snap.png
new file mode 100644
index 0000000000..49f9ad800b
Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-mixed-charts-fitting-functions-non-stacked-series-visually-looks-correct-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-2-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-2-snap.png
new file mode 100644
index 0000000000..874341b22b
Binary files /dev/null and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-2-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png
new file mode 100644
index 0000000000..1073d00e16
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png
new file mode 100644
index 0000000000..8bbf926bec
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png
new file mode 100644
index 0000000000..03c0c7ae28
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png
new file mode 100644
index 0000000000..71ecc0c0f0
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png
new file mode 100644
index 0000000000..57ffc660a5
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png
new file mode 100644
index 0000000000..2a17078745
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png
new file mode 100644
index 0000000000..ef63804ab2
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png
new file mode 100644
index 0000000000..df37f50c84
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-average-1-snap.png
new file mode 100644
index 0000000000..1f9b3b14ba
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-average-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-carry-1-snap.png
new file mode 100644
index 0000000000..fe13aacb14
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-carry-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-explicit-1-snap.png
new file mode 100644
index 0000000000..03c0c7ae28
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-explicit-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-linear-1-snap.png
new file mode 100644
index 0000000000..179f089fc6
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-linear-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-lookahead-1-snap.png
new file mode 100644
index 0000000000..e502d2bf7d
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-lookahead-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-nearest-1-snap.png
new file mode 100644
index 0000000000..2a17078745
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-nearest-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-none-1-snap.png
new file mode 100644
index 0000000000..2f24132a79
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-none-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-zero-1-snap.png
new file mode 100644
index 0000000000..df37f50c84
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-zero-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-average-1-snap.png
new file mode 100644
index 0000000000..49f9ad800b
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-average-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png
new file mode 100644
index 0000000000..a23ceb9594
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png
new file mode 100644
index 0000000000..03c0c7ae28
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png
new file mode 100644
index 0000000000..f4eee894a6
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png
new file mode 100644
index 0000000000..3152e341f9
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png
new file mode 100644
index 0000000000..2a17078745
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-none-1-snap.png
new file mode 100644
index 0000000000..ef63804ab2
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-none-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png
new file mode 100644
index 0000000000..df37f50c84
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-average-1-snap.png
new file mode 100644
index 0000000000..99fdde5816
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-average-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png
new file mode 100644
index 0000000000..6433f597a7
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png
new file mode 100644
index 0000000000..f296c81817
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png
new file mode 100644
index 0000000000..587c4c4094
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png
new file mode 100644
index 0000000000..5f3be43a8c
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png
new file mode 100644
index 0000000000..4ae788a2f5
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-none-1-snap.png
new file mode 100644
index 0000000000..4a4a07a18e
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-none-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png
new file mode 100644
index 0000000000..5520db6a6a
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png
new file mode 100644
index 0000000000..1073d00e16
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png
new file mode 100644
index 0000000000..8bbf926bec
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png
new file mode 100644
index 0000000000..03c0c7ae28
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png
new file mode 100644
index 0000000000..71ecc0c0f0
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png
new file mode 100644
index 0000000000..57ffc660a5
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png
new file mode 100644
index 0000000000..2a17078745
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png
new file mode 100644
index 0000000000..ef63804ab2
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png
new file mode 100644
index 0000000000..df37f50c84
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png
new file mode 100644
index 0000000000..ff328cfec9
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png
new file mode 100644
index 0000000000..0c41dfe5b1
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png
new file mode 100644
index 0000000000..40f3e66fe5
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png
new file mode 100644
index 0000000000..d89b6e5aed
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png
new file mode 100644
index 0000000000..936c6fc8ee
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png
new file mode 100644
index 0000000000..d46d71eb5b
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png
new file mode 100644
index 0000000000..a2f2eb5370
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png
new file mode 100644
index 0000000000..527a9f603b
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-average-1-snap.png
new file mode 100644
index 0000000000..81f62480fb
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-average-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png
new file mode 100644
index 0000000000..4a49c5f6d8
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png
new file mode 100644
index 0000000000..40f3e66fe5
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png
new file mode 100644
index 0000000000..78ba866112
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png
new file mode 100644
index 0000000000..0e142da1bc
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png
new file mode 100644
index 0000000000..1bc9eeb764
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-none-1-snap.png
new file mode 100644
index 0000000000..a2f2eb5370
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-none-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png
new file mode 100644
index 0000000000..527a9f603b
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png
new file mode 100644
index 0000000000..ff328cfec9
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png
new file mode 100644
index 0000000000..0c41dfe5b1
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png
new file mode 100644
index 0000000000..40f3e66fe5
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png
new file mode 100644
index 0000000000..d89b6e5aed
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png
new file mode 100644
index 0000000000..936c6fc8ee
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png
new file mode 100644
index 0000000000..d46d71eb5b
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png
new file mode 100644
index 0000000000..a2f2eb5370
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png differ
diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png
new file mode 100644
index 0000000000..527a9f603b
Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png differ
diff --git a/integration/tests/all.test.ts b/integration/tests/all.test.ts
index 46899d746e..a3dfb202ba 100644
--- a/integration/tests/all.test.ts
+++ b/integration/tests/all.test.ts
@@ -14,7 +14,8 @@ describe('Baseline Visual tests for all stories', () => {
stories.forEach(({ title, encodedTitle }) => {
describe(title, () => {
it('visually looks correct', async () => {
- await common.expectChartAtUrlToMatchScreenshot(`http://localhost:9001?id=${encodedGroup}--${encodedTitle}`);
+ const url = `http://localhost:9001?id=${encodedGroup}--${encodedTitle}`;
+ await common.expectChartAtUrlToMatchScreenshot(url);
});
});
});
diff --git a/integration/tests/mixed-stories.test.ts b/integration/tests/mixed-stories.test.ts
new file mode 100644
index 0000000000..855fa25c23
--- /dev/null
+++ b/integration/tests/mixed-stories.test.ts
@@ -0,0 +1,86 @@
+import { common } from '../page_objects';
+import { Fit } from '../../src/chart_types/xy_chart/utils/specs';
+
+describe('Mixed series stories', () => {
+ describe('Fitting functions', () => {
+ describe('Area charts - no endValue', () => {
+ Object.values(Fit).forEach((fitType) => {
+ it(`should display correct fit for type - ${fitType}`, async () => {
+ await common.expectChartAtUrlToMatchScreenshot(
+ `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=area&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=0&knob-End value=none&knob-Explicit valuve (using Fit.Explicit)=8`,
+ );
+ });
+ });
+ });
+
+ describe('Area charts - endValue set to 2', () => {
+ Object.values(Fit).forEach((fitType) => {
+ it(`should display correct fit for type - ${fitType}`, async () => {
+ await common.expectChartAtUrlToMatchScreenshot(
+ `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=area&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=0&knob-End value=2&knob-Explicit valuve (using Fit.Explicit)=8`,
+ );
+ });
+ });
+ });
+
+ describe('Area charts - endValue set to "nearest"', () => {
+ Object.values(Fit).forEach((fitType) => {
+ it(`should display correct fit for type - ${fitType}`, async () => {
+ await common.expectChartAtUrlToMatchScreenshot(
+ `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=area&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=0&knob-End value=nearest&knob-Explicit valuve (using Fit.Explicit)=8`,
+ );
+ });
+ });
+ });
+
+ describe('Area charts - with curved - endValue set to 2', () => {
+ Object.values(Fit).forEach((fitType) => {
+ it(`should display correct fit for type - ${fitType}`, async () => {
+ await common.expectChartAtUrlToMatchScreenshot(
+ `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=area&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=1&knob-End value=2&knob-Explicit valuve (using Fit.Explicit)=8`,
+ );
+ });
+ });
+ });
+
+ describe('Area charts - Ordinal dataset - no endValue', () => {
+ Object.values(Fit).forEach((fitType) => {
+ it(`should display correct fit for type - ${fitType}`, async () => {
+ await common.expectChartAtUrlToMatchScreenshot(
+ `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=area&knob-dataset=ordinal&knob-fitting function=${fitType}&knob-Curve=0&knob-End value=none&knob-Explicit valuve (using Fit.Explicit)=8`,
+ );
+ });
+ });
+ });
+
+ describe('Line charts - no endValue', () => {
+ Object.values(Fit).forEach((fitType) => {
+ it(`should display correct fit for type - ${fitType}`, async () => {
+ await common.expectChartAtUrlToMatchScreenshot(
+ `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=line&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=0&knob-End value=none&knob-Explicit valuve (using Fit.Explicit)=8`,
+ );
+ });
+ });
+ });
+
+ describe('Line charts - endValue set to 2', () => {
+ Object.values(Fit).forEach((fitType) => {
+ it(`should display correct fit for type - ${fitType}`, async () => {
+ await common.expectChartAtUrlToMatchScreenshot(
+ `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=line&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=0&knob-End value=2&knob-Explicit valuve (using Fit.Explicit)=8`,
+ );
+ });
+ });
+ });
+
+ describe('Line charts - with curve - endValue set to 2', () => {
+ Object.values(Fit).forEach((fitType) => {
+ it(`should display correct fit for type - ${fitType}`, async () => {
+ await common.expectChartAtUrlToMatchScreenshot(
+ `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=line&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=1&knob-End value=2&knob-Explicit valuve (using Fit.Explicit)=8`,
+ );
+ });
+ });
+ });
+ });
+});
diff --git a/jest.config.js b/jest.config.js
index 49155c8540..726b4296e1 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -2,7 +2,9 @@ module.exports = {
roots: ['/src'],
preset: 'ts-jest',
testEnvironment: 'jest-environment-jsdom-fourteen',
- setupFilesAfterEnv: ['/scripts/setup_enzyme.ts'],
+ setupFilesAfterEnv: ['jest-extended', '/scripts/setup_enzyme.ts', '/scripts/custom_matchers.ts'],
+ coveragePathIgnorePatterns: ['/src/mocks/', '/node_modules/'],
+ clearMocks: true,
globals: {
'ts-jest': {
tsConfig: 'tsconfig.jest.json',
diff --git a/package.json b/package.json
index 39fad2c58a..0a8a989a56 100644
--- a/package.json
+++ b/package.json
@@ -25,19 +25,19 @@
"storybook:build": "rm -rf .out && build-storybook -c .storybook -o .out",
"lint": "NODE_ENV=production eslint --ext .tsx,.ts,.js .",
"lint:fix": "yarn lint --fix",
+ "test": "jest --verbose --config jest.config.js",
"pq": "pretty-quick",
- "test": "jest --config jest.config.js",
"test:tz": "yarn test:tz-utc && yarn test:tz-ny && yarn test:tz-jp",
- "test:tz-utc": "TZ=UTC jest --config=jest.tz.config.js",
- "test:tz-ny": "TZ=America/New_York jest --config=jest.tz.config.js",
- "test:tz-jp": "TZ=Asia/Tokyo jest --config=jest.tz.config.js",
+ "test:tz-utc": "TZ=UTC jest --verbose --config=jest.tz.config.js",
+ "test:tz-ny": "TZ=America/New_York jest --verbose --config=jest.tz.config.js",
+ "test:tz-jp": "TZ=Asia/Tokyo jest --verbose --config=jest.tz.config.js",
"watch": "yarn test --watch",
"semantic-release": "semantic-release",
"typecheck:src": "yarn build:ts --noEmit",
"typecheck:all": "tsc -p ./tsconfig.json --noEmit",
"playground": "cd .playground && webpack-dev-server",
"playground:ie": "cd .playground && webpack-dev-server --host=0.0.0.0 --disable-host-check --useLocalIp",
- "jest:integration": "TZ=UTC JEST_PUPPETEER_CONFIG=integration/jest-puppeteer.config.js jest --rootDir=integration -c=integration/jest.config.js --runInBand"
+ "jest:integration": "TZ=UTC JEST_PUPPETEER_CONFIG=integration/jest-puppeteer.config.js jest --verbose --rootDir=integration -c=integration/jest.config.js --runInBand"
},
"files": [
"dist/**/*",
@@ -110,9 +110,12 @@
"husky": "^1.3.1",
"jest": "^24.9.0",
"jest-environment-jsdom-fourteen": "^0.1.0",
+ "jest-extended": "^0.11.2",
"jest-image-snapshot": "^2.11.0",
+ "jest-matcher-utils": "^24.9.0",
"jest-puppeteer": "^4.3.0",
"jest-puppeteer-docker": "^1.2.0",
+ "lodash": "^4.17.15",
"lorem-ipsum": "^2.0.3",
"luxon": "^1.11.3",
"moment-timezone": "^0.5.27",
@@ -132,6 +135,7 @@
"ts-jest": "^24.1.0",
"ts-loader": "^6.1.2",
"typescript": "^3.6.3",
+ "utility-types": "^3.8.0",
"webpack": "^4.29.5",
"webpack-cli": "^3.3.1",
"webpack-dev-server": "^3.3.1"
diff --git a/scripts/custom_matchers.ts b/scripts/custom_matchers.ts
new file mode 100644
index 0000000000..bf6d226a8e
--- /dev/null
+++ b/scripts/custom_matchers.ts
@@ -0,0 +1,70 @@
+import { matcherErrorMessage } from 'jest-matcher-utils';
+
+// ensure this is parsed as a module.
+export {};
+
+/**
+ * Final Matcher type with `this` and `received` args removed from jest matcher function
+ */
+type MatcherParameters any> = T extends (
+ this: any,
+ received: any,
+ ...args: infer P
+) => any
+ ? P
+ : never;
+
+declare global {
+ namespace jest { // eslint-disable-line
+ interface Matchers {
+ /**
+ * Expect array to be filled with value, and optionally length
+ */
+ toEqualArrayOf(...args: MatcherParameters): R;
+ }
+ }
+}
+
+/**
+ * Expect array to be filled with value, and optionally length
+ */
+function toEqualArrayOf(this: jest.MatcherUtils, received: any[], value: any, length?: number) {
+ const matcherName = 'toEqualArrayOf';
+
+ if (!Array.isArray(received)) {
+ throw new Error(
+ matcherErrorMessage(
+ this.utils.matcherHint(matcherName),
+ `${this.utils.RECEIVED_COLOR('received')} value must be an array.`,
+ `Received type: ${typeof received}`,
+ ),
+ );
+ }
+
+ const receivedPretty = this.utils.printReceived(received);
+ const elementCheck = received.every((v) => v === value);
+ const lengthCheck = length === undefined || received.length === length;
+
+ if (!lengthCheck) {
+ return {
+ pass: false,
+ message: () => `expected array length to be ${length} but got ${received.length}`,
+ };
+ }
+
+ if (!elementCheck) {
+ return {
+ pass: false,
+ message: () => `expected ${receivedPretty} to be an array of ${value}'s`,
+ };
+ }
+
+ return {
+ pass: true,
+ message: () => `expected ${receivedPretty} not to be an array of ${value}'s`,
+ };
+}
+
+expect.extend({
+ toEqualArrayOf,
+});
diff --git a/src/chart_types/xy_chart/rendering/rendering.test.ts b/src/chart_types/xy_chart/rendering/rendering.test.ts
index 4ae2eff003..45b0997b5e 100644
--- a/src/chart_types/xy_chart/rendering/rendering.test.ts
+++ b/src/chart_types/xy_chart/rendering/rendering.test.ts
@@ -7,10 +7,13 @@ import {
getBarStyleOverrides,
GeometryId,
getPointStyleOverrides,
+ getClippedRanges,
} from './rendering';
import { BarSeriesStyle, SharedGeometryStateStyle, PointStyle } from '../../../utils/themes/theme';
import { DataSeriesDatum } from '../utils/series';
import { RecursivePartial, mergePartial } from '../../../utils/commons';
+import { MockDataSeries } from '../../../mocks';
+import { MockScale } from '../../../mocks/scale';
describe('Rendering utils', () => {
test('check if point is in geometry', () => {
@@ -330,4 +333,56 @@ describe('Rendering utils', () => {
expect(styleOverrides).toEqual(expectedStyles);
});
});
+
+ describe('getClippedRanges', () => {
+ const dataSeries = MockDataSeries.fitFunction({ shuffle: false });
+ const xScale = MockScale.default({
+ scale: jest.fn().mockImplementation((x) => x),
+ bandwidth: 0,
+ range: [dataSeries.data[0].x as number, dataSeries.data[12].x as number],
+ });
+
+ it('should return array pairs of non-null x regions with null end values', () => {
+ const actual = getClippedRanges(dataSeries.data, xScale, 0);
+
+ expect(actual).toEqual([[0, 1], [2, 4], [4, 6], [7, 11], [11, 12]]);
+ });
+
+ it('should return array pairs of non-null x regions with valid end values', () => {
+ const data = dataSeries.data.slice(1, -1);
+ const xScale = MockScale.default({
+ scale: jest.fn().mockImplementation((x) => x),
+ range: [data[0].x as number, data[10].x as number],
+ });
+ const actual = getClippedRanges(data, xScale, 0);
+
+ expect(actual).toEqual([[2, 4], [4, 6], [7, 11]]);
+ });
+
+ it('should account for bandwidth', () => {
+ const bandwidth = 2;
+ const xScale = MockScale.default({
+ scale: jest.fn().mockImplementation((x) => x),
+ bandwidth,
+ range: [dataSeries.data[0].x as number, (dataSeries.data[12].x as number) + bandwidth * (2 / 3)],
+ });
+ const actual = getClippedRanges(dataSeries.data, xScale, 0);
+
+ expect(actual).toEqual([[0, 2], [3, 5], [5, 7], [8, 12]]);
+ });
+
+ it('should account for xScaleOffset', () => {
+ const actual = getClippedRanges(dataSeries.data, xScale, 2);
+
+ expect(actual).toEqual([[0, -1], [0, 2], [2, 4], [5, 9]]);
+ });
+
+ it('should call scale to get x value for each datum', () => {
+ getClippedRanges(dataSeries.data, xScale, 0);
+
+ expect(xScale.scale).toHaveBeenNthCalledWith(1, dataSeries.data[0].x);
+ expect(xScale.scale).toHaveBeenCalledTimes(dataSeries.data.length);
+ expect(xScale.scale).toHaveBeenCalledWith(dataSeries.data[12].x);
+ });
+ });
});
diff --git a/src/chart_types/xy_chart/rendering/rendering.ts b/src/chart_types/xy_chart/rendering/rendering.ts
index 03a3b6b7f0..7565f5b6c3 100644
--- a/src/chart_types/xy_chart/rendering/rendering.ts
+++ b/src/chart_types/xy_chart/rendering/rendering.ts
@@ -1,4 +1,5 @@
import { area, line } from 'd3-shape';
+import { $Values } from 'utility-types';
import { CanvasTextBBoxCalculator } from '../../../utils/bbox/canvas_text_bbox_calculator';
import {
@@ -34,7 +35,7 @@ export const AccessorType = Object.freeze({
Y1: 'y1' as 'y1',
});
-export type AccessorType = typeof AccessorType.Y0 | typeof AccessorType.Y1;
+export type AccessorType = $Values;
export interface GeometryValue {
y: any;
@@ -44,6 +45,13 @@ export interface GeometryValue {
export type IndexedGeometry = PointGeometry | BarGeometry;
+/**
+ * Array of **range** clippings [x1, x2] to be excluded during rendering
+ *
+ * Note: Must be scaled **range** values (i.e. pixel coordinates) **NOT** domain values
+ */
+export type ClippedRanges = [number, number][];
+
export interface PointGeometry {
x: number;
y: number;
@@ -85,6 +93,10 @@ export interface LineGeometry {
geometryId: GeometryId;
seriesLineStyle: LineStyle;
seriesPointStyle: PointStyle;
+ /**
+ * Ranges of `[x0, x1]` pairs to clip from series
+ */
+ clippedRanges: ClippedRanges;
}
export interface AreaGeometry {
area: string;
@@ -100,6 +112,10 @@ export interface AreaGeometry {
seriesAreaLineStyle: LineStyle;
seriesPointStyle: PointStyle;
isStacked: boolean;
+ /**
+ * Ranges of `[x0, x1]` pairs to clip from series
+ */
+ clippedRanges: ClippedRanges;
}
export function isPointGeometry(ig: IndexedGeometry): ig is PointGeometry {
@@ -407,6 +423,7 @@ export function renderLine(
xScaleOffset: number,
seriesStyle: LineSeriesStyle,
pointStyleAccessor?: PointStyleAccessor,
+ hasFit?: boolean,
): {
lineGeometry: LineGeometry;
indexedGeometries: Map;
@@ -415,15 +432,19 @@ export function renderLine(
const pathGenerator = line()
.x(({ x }) => xScale.scale(x) - xScaleOffset)
- .y(({ y1 }) => {
- if (y1 !== null) {
- return yScale.scale(y1);
+ .y((datum) => {
+ const yValue = getYValue(datum);
+
+ if (yValue !== null) {
+ return yScale.scale(yValue);
}
+
// this should never happen thanks to the defined function
return yScale.isInverted ? yScale.range[1] : yScale.range[0];
})
- .defined(({ x, y1 }) => {
- return y1 !== null && !(isLogScale && y1 <= 0) && xScale.isValueInDomain(x);
+ .defined((datum) => {
+ const yValue = getYValue(datum);
+ return yValue !== null && !(isLogScale && yValue <= 0) && xScale.isValueInDomain(datum.x);
})
.curve(getCurveFactory(curve));
const y = 0;
@@ -441,6 +462,7 @@ export function renderLine(
pointStyleAccessor,
);
+ const clippedRanges = hasFit && !hasY0Accessors ? getClippedRanges(dataset, xScale, xScaleOffset) : [];
const lineGeometry = {
line: pathGenerator(dataset) || '',
points: pointGeometries,
@@ -455,6 +477,7 @@ export function renderLine(
},
seriesLineStyle: seriesStyle.line,
seriesPointStyle: seriesStyle.point,
+ clippedRanges,
};
return {
lineGeometry,
@@ -462,6 +485,21 @@ export function renderLine(
};
}
+/**
+ * Returns value of `y1` or `filled.y1` or null
+ */
+export const getYValue = ({ y1, filled }: DataSeriesDatum): number | null => {
+ if (y1 !== null) {
+ return y1;
+ }
+
+ if (filled && filled.y1 !== undefined) {
+ return filled.y1;
+ }
+
+ return null;
+};
+
export function renderArea(
shift: number,
dataset: DataSeriesDatum[],
@@ -476,17 +514,18 @@ export function renderArea(
seriesStyle: AreaSeriesStyle,
isStacked = false,
pointStyleAccessor?: PointStyleAccessor,
+ hasFit?: boolean,
): {
areaGeometry: AreaGeometry;
indexedGeometries: Map;
} {
const isLogScale = isLogarithmicScale(yScale);
-
const pathGenerator = area()
.x(({ x }) => xScale.scale(x) - xScaleOffset)
- .y1(({ y1 }) => {
- if (y1 !== null) {
- return yScale.scale(y1);
+ .y1((datum) => {
+ const yValue = getYValue(datum);
+ if (yValue !== null) {
+ return yScale.scale(yValue);
}
// this should never happen thanks to the defined function
return yScale.isInverted ? yScale.range[1] : yScale.range[0];
@@ -497,13 +536,14 @@ export function renderArea(
}
return yScale.scale(y0);
})
- .defined(({ y1, x }) => {
- return y1 !== null && !(isLogScale && y1 <= 0) && xScale.isValueInDomain(x);
+ .defined((datum) => {
+ const yValue = getYValue(datum);
+ return yValue !== null && !(isLogScale && yValue <= 0) && xScale.isValueInDomain(datum.x);
})
.curve(getCurveFactory(curve));
+ const clippedRanges = hasFit && !hasY0Accessors && !isStacked ? getClippedRanges(dataset, xScale, xScaleOffset) : [];
const y1Line = pathGenerator.lineY1()(dataset);
-
const lines: string[] = [];
if (y1Line) {
lines.push(y1Line);
@@ -527,7 +567,7 @@ export function renderArea(
pointStyleAccessor,
);
- const areaGeometry = {
+ const areaGeometry: AreaGeometry = {
area: pathGenerator(dataset) || '',
lines,
points: pointGeometries,
@@ -544,6 +584,7 @@ export function renderArea(
seriesAreaLineStyle: seriesStyle.line,
seriesPointStyle: seriesStyle.point,
isStacked,
+ clippedRanges,
};
return {
areaGeometry,
@@ -551,6 +592,41 @@ export function renderArea(
};
}
+/**
+ * Gets clipped ranges that have been fitted to values
+ * @param dataset
+ * @param xScale
+ * @param xScaleOffset
+ */
+export function getClippedRanges(dataset: DataSeriesDatum[], xScale: Scale, xScaleOffset: number): ClippedRanges {
+ let firstNonNullX: number | null = null;
+ let hasNull = false;
+
+ return dataset.reduce((acc, { x, y1 }) => {
+ const xValue = xScale.scale(x) - xScaleOffset + xScale.bandwidth / 2;
+
+ if (y1 !== null) {
+ if (hasNull) {
+ if (firstNonNullX !== null) {
+ acc.push([firstNonNullX, xValue]);
+ } else {
+ acc.push([0, xValue]);
+ }
+ hasNull = false;
+ }
+
+ firstNonNullX = xValue;
+ } else {
+ const endXValue = xScale.range[1] - xScale.bandwidth * (2 / 3);
+ if (firstNonNullX !== null && xValue === endXValue) {
+ acc.push([firstNonNullX, xValue]);
+ }
+ hasNull = true;
+ }
+ return acc;
+ }, []);
+}
+
export function getGeometryStateStyle(
geometryId: GeometryId,
highlightedLegendItem: LegendItem | null,
diff --git a/src/chart_types/xy_chart/store/utils.ts b/src/chart_types/xy_chart/store/utils.ts
index eb57d600b2..b4c3e2f637 100644
--- a/src/chart_types/xy_chart/store/utils.ts
+++ b/src/chart_types/xy_chart/store/utils.ts
@@ -36,6 +36,8 @@ import {
LineSeriesSpec,
Rotation,
isBandedSpec,
+ Fit,
+ FitConfig,
} from '../utils/specs';
import { ColorConfig, Theme } from '../../../utils/themes/theme';
import { identity, mergePartial } from '../../../utils/commons';
@@ -165,7 +167,13 @@ export function computeSeriesDomains(
const xDomain = mergeXDomain(specsArray, xValues, customXDomain);
const yDomain = mergeYDomain(splittedSeries, specsArray, customYDomainsByGroupId);
- const formattedDataSeries = getFormattedDataseries(specsArray, splittedSeries, xValues, xDomain.scaleType);
+ const formattedDataSeries = getFormattedDataseries(
+ specsArray,
+ splittedSeries,
+ xValues,
+ xDomain.scaleType,
+ seriesSpecs,
+ );
// we need to get the last values from the formatted dataseries
// because we change the format if we are on percentage mode
@@ -493,6 +501,7 @@ export function renderGeometries(
xScaleOffset,
lineSeriesStyle,
spec.pointStyleAccessor,
+ Boolean(spec.fit && ((spec.fit as FitConfig).type || spec.fit) !== Fit.None),
);
lineGeometriesIndex = mergeGeometriesIndexes(lineGeometriesIndex, renderedLines.indexedGeometries);
lines.push(renderedLines.lineGeometry);
@@ -520,6 +529,7 @@ export function renderGeometries(
areaSeriesStyle,
isStacked,
spec.pointStyleAccessor,
+ Boolean(spec.fit && ((spec.fit as FitConfig).type || spec.fit) !== Fit.None),
);
areaGeometriesIndex = mergeGeometriesIndexes(areaGeometriesIndex, renderedAreas.indexedGeometries);
areas.push(renderedAreas.areaGeometry);
diff --git a/src/chart_types/xy_chart/utils/fit_function.test.ts b/src/chart_types/xy_chart/utils/fit_function.test.ts
new file mode 100644
index 0000000000..2cd40e95a8
--- /dev/null
+++ b/src/chart_types/xy_chart/utils/fit_function.test.ts
@@ -0,0 +1,1002 @@
+import {
+ MockDataSeries,
+ getFilledNullData,
+ getFilledNonNullData,
+ getYResolvedData,
+ MockDataSeriesDatum,
+} from '../../../mocks';
+import { Fit } from './specs';
+import { ScaleType } from '../../../utils/scales/scales';
+import { DataSeries } from './series';
+
+import * as seriesUtils from './stacked_series_utils';
+import * as testModule from './fit_function';
+
+describe('Fit Function', () => {
+ describe('getValue', () => {
+ describe('Non-Ordinal scale', () => {
+ it('should return current datum if next and previous are null with no endValue', () => {
+ const current = MockDataSeriesDatum.default();
+ const actual = testModule.getValue(current, 0, null, null, Fit.Average);
+
+ expect(actual).toBe(current);
+ expect(actual.filled).toBeUndefined();
+ });
+
+ it('should return current datum with filled endValue if next and previous are null with endValue', () => {
+ const current = MockDataSeriesDatum.default();
+ const actual = testModule.getValue(current, 0, null, null, Fit.Average, 100);
+
+ expect(actual.filled!.y1).toBe(100);
+ });
+
+ describe('previous is not null and fit type is Carry', () => {
+ it('should return current datum with value from next when previous is null', () => {
+ const previous = MockDataSeriesDatum.full({ x: 0, y1: 20 });
+ const current = MockDataSeriesDatum.simple({ x: 3 });
+ const actual = testModule.getValue(current, 0, previous, null, Fit.Carry);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+ });
+
+ describe('next is not null and fit type is Lookahead', () => {
+ it('should return current datum with value from next when previous is null', () => {
+ const current = MockDataSeriesDatum.simple({ x: 3 });
+ const next = MockDataSeriesDatum.full({ x: 4, y1: 20 });
+ const actual = testModule.getValue(current, 0, null, next, Fit.Lookahead);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+ });
+
+ describe('current and previous datums are not null', () => {
+ describe('Average - fit type', () => {
+ it('should return current datum with average values from previous and next', () => {
+ const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 });
+ const current = MockDataSeriesDatum.simple({ x: 3 });
+ const next = MockDataSeriesDatum.full({ x: 4, y1: 20 });
+ const actual = testModule.getValue(current, 0, previous, next, Fit.Average);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: (10 + 20) / 2,
+ });
+ });
+ });
+
+ describe('Nearest - fit type', () => {
+ it('should return current datum with values from previous not next', () => {
+ const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 });
+ const current = MockDataSeriesDatum.simple({ x: 3 });
+ const next = MockDataSeriesDatum.full({ x: 10, y1: 20 });
+ const actual = testModule.getValue(current, 0, previous, next, Fit.Nearest);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 10,
+ });
+ });
+
+ it('should return current datum with values from next not previous', () => {
+ const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 });
+ const current = MockDataSeriesDatum.simple({ x: 9 });
+ const next = MockDataSeriesDatum.full({ x: 10, y1: 20 });
+ const actual = testModule.getValue(current, 0, previous, next, Fit.Nearest);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+ });
+
+ describe('Linear - fit type', () => {
+ it('should return average from equidistant previous and next datums', () => {
+ const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 });
+ const current = MockDataSeriesDatum.simple({ x: 5 });
+ const next = MockDataSeriesDatum.full({ x: 10, y1: 20 });
+ const actual = testModule.getValue(current, 0, previous, next, Fit.Linear);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: (10 + 20) / 2,
+ });
+ });
+
+ it('should return positive slope interpolated value from equidistant previous and next datums', () => {
+ const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 });
+ const current = MockDataSeriesDatum.simple({ x: 3 });
+ const next = MockDataSeriesDatum.full({ x: 10, y1: 20 });
+ const actual = testModule.getValue(current, 0, previous, next, Fit.Linear);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 13,
+ });
+ });
+
+ it('should return negative slope interpolated value from equidistant previous and next datums', () => {
+ const previous = MockDataSeriesDatum.full({ x: 0, y1: 20 });
+ const current = MockDataSeriesDatum.simple({ x: 3 });
+ const next = MockDataSeriesDatum.full({ x: 10, y1: 10 });
+ const actual = testModule.getValue(current, 0, previous, next, Fit.Linear);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 17,
+ });
+ });
+
+ it('should return complex interpolated value from equidistant previous and next datums', () => {
+ const previous = MockDataSeriesDatum.full({ x: 0.767, y1: 10.545 });
+ const current = MockDataSeriesDatum.simple({ x: 3.564 });
+ const next = MockDataSeriesDatum.full({ x: 10.767, y1: 20.657 });
+ const actual = testModule.getValue(current, 0, previous, next, Fit.Linear);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 13.3733264,
+ });
+ });
+ });
+ });
+
+ describe('next or previous datums are not null - with fits requring bounding datums', () => {
+ describe('Nearest - fit type', () => {
+ it('should return current datum with value from next when previous is null', () => {
+ const current = MockDataSeriesDatum.simple({ x: 3 });
+ const next = MockDataSeriesDatum.full({ x: 4, y1: 20 });
+ const actual = testModule.getValue(current, 0, null, next, Fit.Nearest);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+
+ it('should return current datum with value from next when previous is null', () => {
+ const previous = MockDataSeriesDatum.full({ x: 4, y1: 20 });
+ const current = MockDataSeriesDatum.simple({ x: 3 });
+ const actual = testModule.getValue(current, 0, previous, null, Fit.Nearest);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+ });
+
+ describe("endValue is set to 'nearest'", () => {
+ it('should return current datum with value from next when previous is null', () => {
+ const current = MockDataSeriesDatum.simple({ x: 3 });
+ const next = MockDataSeriesDatum.full({ x: 4, y1: 20 });
+ const actual = testModule.getValue(current, 0, null, next, Fit.Average, 'nearest');
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+
+ it('should return current datum with value from next when previous is null', () => {
+ const previous = MockDataSeriesDatum.full({ x: 0, y1: 20 });
+ const current = MockDataSeriesDatum.simple({ x: 3 });
+ const actual = testModule.getValue(current, 0, previous, null, Fit.Average, 'nearest');
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+ });
+ });
+ });
+ describe('Ordinal scale', () => {
+ it('should return current datum if next and previous are null with no endValue', () => {
+ const current = MockDataSeriesDatum.ordinal();
+ const actual = testModule.getValue(current, 0, null, null, Fit.Average);
+
+ expect(actual).toBe(current);
+ expect(actual.filled).toBeUndefined();
+ });
+
+ it('should return current datum with filled endValue if next and previous are null with endValue', () => {
+ const current = MockDataSeriesDatum.ordinal();
+ const actual = testModule.getValue(current, 0, null, null, Fit.Average, 100);
+
+ expect(actual.filled!.y1).toBe(100);
+ });
+
+ describe('previous is not null and fit type is Carry', () => {
+ it('should return current datum with value from next when previous is null', () => {
+ const previous = MockDataSeriesDatum.full({ x: 'a', y1: 20, fittingIndex: 0 });
+ const current = MockDataSeriesDatum.simple({ x: 'c' });
+ const actual = testModule.getValue(current, 3, previous, null, Fit.Carry);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+ });
+
+ describe('next is not null and fit type is Lookahead', () => {
+ it('should return current datum with value from next when previous is null', () => {
+ const current = MockDataSeriesDatum.simple({ x: 'c' });
+ const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 4 });
+ const actual = testModule.getValue(current, 3, null, next, Fit.Lookahead);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+ });
+
+ describe('current and previous datums are not null', () => {
+ describe('Average - fit type', () => {
+ it('should return current datum with average values from previous and next', () => {
+ const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 });
+ const current = MockDataSeriesDatum.simple({ x: 'c' });
+ const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 4 });
+ const actual = testModule.getValue(current, 3, previous, next, Fit.Average);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: (10 + 20) / 2,
+ });
+ });
+ });
+
+ describe('Nearest - fit type', () => {
+ it('should return current datum with values from previous not next', () => {
+ const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 });
+ const current = MockDataSeriesDatum.simple({ x: 'c' });
+ const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 10 });
+ const actual = testModule.getValue(current, 3, previous, next, Fit.Nearest);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 10,
+ });
+ });
+
+ it('should return current datum with values from next not previous', () => {
+ const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 });
+ const current = MockDataSeriesDatum.simple({ x: 'c' });
+ const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 10 });
+ const actual = testModule.getValue(current, 9, previous, next, Fit.Nearest);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+ });
+
+ describe('Linear - fit type', () => {
+ it('should return average from equidistant previous and next datums', () => {
+ const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 });
+ const current = MockDataSeriesDatum.simple({ x: 'c' });
+ const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 10 });
+ const actual = testModule.getValue(current, 5, previous, next, Fit.Linear);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: (10 + 20) / 2,
+ });
+ });
+
+ it('should return positive slope interpolated value from equidistant previous and next datums', () => {
+ const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 });
+ const current = MockDataSeriesDatum.simple({ x: 'c' });
+ const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 10 });
+ const actual = testModule.getValue(current, 3, previous, next, Fit.Linear);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 13,
+ });
+ });
+
+ it('should return negative slope interpolated value from equidistant previous and next datums', () => {
+ const previous = MockDataSeriesDatum.full({ x: 'a', y1: 20, fittingIndex: 0 });
+ const current = MockDataSeriesDatum.simple({ x: 'c' });
+ const next = MockDataSeriesDatum.full({ x: 'e', y1: 10, fittingIndex: 10 });
+ const actual = testModule.getValue(current, 3, previous, next, Fit.Linear);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 17,
+ });
+ });
+
+ it('should return complex interpolated value from equidistant previous and next datums', () => {
+ const previous = MockDataSeriesDatum.full({ x: 'a', fittingIndex: 0.767, y1: 10.545 });
+ const current = MockDataSeriesDatum.simple({ x: 'c' });
+ const next = MockDataSeriesDatum.full({ x: 'e', fittingIndex: 10.767, y1: 20.657 });
+ const actual = testModule.getValue(current, 3.564, previous, next, Fit.Linear);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 13.3733264,
+ });
+ });
+ });
+ });
+
+ describe('next or previous datums are not null - with fits requring bounding datums', () => {
+ describe('Nearest - fit type', () => {
+ it('should return current datum with value from next when previous is null', () => {
+ const current = MockDataSeriesDatum.simple({ x: 'c' });
+ const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 4 });
+ const actual = testModule.getValue(current, 3, null, next, Fit.Nearest);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+
+ it('should return current datum with value from next when previous is null', () => {
+ const previous = MockDataSeriesDatum.full({ x: 4, y1: 20, fittingIndex: 0 });
+ const current = MockDataSeriesDatum.simple({ x: 'c' });
+ const actual = testModule.getValue(current, 3, previous, null, Fit.Nearest);
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+ });
+
+ describe("endValue is set to 'nearest'", () => {
+ it('should return current datum with value from next when previous is null', () => {
+ const current = MockDataSeriesDatum.simple({ x: 'c' });
+ const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 4 });
+ const actual = testModule.getValue(current, 3, null, next, Fit.Average, 'nearest');
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+
+ it('should return current datum with value from next when previous is null', () => {
+ const previous = MockDataSeriesDatum.full({ x: 4, y1: 20, fittingIndex: 0 });
+ const current = MockDataSeriesDatum.simple({ x: 'c' });
+ const actual = testModule.getValue(current, 3, previous, null, Fit.Average, 'nearest');
+
+ expect(actual).toMatchObject(current);
+ expect(actual.filled).toEqual({
+ y1: 20,
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('parseConfig', () => {
+ it('should return default type when none exists', () => {
+ const actual = testModule.parseConfig();
+
+ expect(actual).toEqual({
+ type: Fit.None,
+ });
+ });
+
+ it('should parse string config', () => {
+ const actual = testModule.parseConfig(Fit.Average);
+
+ expect(actual).toEqual({
+ type: Fit.Average,
+ });
+ });
+
+ it('should return default when Explicit is passes without value', () => {
+ const actual = testModule.parseConfig({ type: Fit.Explicit });
+
+ expect(actual).toEqual({
+ type: Fit.None,
+ });
+ });
+
+ it('should return type and value when Explicit is passes with value', () => {
+ const actual = testModule.parseConfig({ type: Fit.Explicit, value: 20 });
+
+ expect(actual).toEqual({
+ type: Fit.Explicit,
+ value: 20,
+ endValue: undefined,
+ });
+ });
+
+ it('should return type when no value or endValue is given', () => {
+ const actual = testModule.parseConfig({ type: Fit.Average });
+
+ expect(actual).toEqual({
+ type: Fit.Average,
+ value: undefined,
+ endValue: undefined,
+ });
+ });
+
+ it('should return type and endValue when endValue is passed', () => {
+ const actual = testModule.parseConfig({ type: Fit.Average, endValue: 20 });
+
+ expect(actual).toEqual({
+ type: Fit.Average,
+ value: undefined,
+ endValue: 20,
+ });
+ });
+ });
+
+ describe('fitFunction', () => {
+ let dataSeries: DataSeries;
+
+ beforeAll(() => {
+ jest.spyOn(testModule, 'parseConfig');
+ jest.spyOn(testModule, 'getValue');
+ dataSeries = MockDataSeries.fitFunction();
+ });
+
+ describe('allow mutliple fit config types', () => {
+ it('should allow string config', () => {
+ testModule.fitFunction(dataSeries, Fit.None, ScaleType.Linear);
+
+ expect(testModule.parseConfig).toHaveBeenCalledWith(Fit.None);
+ expect(testModule.parseConfig).toHaveBeenCalledTimes(1);
+ });
+
+ it('should allow object config', () => {
+ const fitConfig = {
+ type: Fit.None,
+ };
+ testModule.fitFunction(dataSeries, fitConfig, ScaleType.Linear);
+
+ expect(testModule.parseConfig).toHaveBeenCalledWith(fitConfig);
+ expect(testModule.parseConfig).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('sorting', () => {
+ const spies: jest.SpyInstance[] = [];
+ const mockArray: any[] = [];
+ // @ts-ignore
+ jest.spyOn(mockArray, 'sort');
+
+ beforeAll(() => {
+ // @ts-ignore
+ spies.push(jest.spyOn(dataSeries.data, 'sort'));
+ // @ts-ignore
+ spies.push(jest.spyOn(dataSeries.data, 'slice').mockReturnValue(mockArray));
+ });
+
+ afterAll(() => {
+ spies.forEach((s) => s.mockRestore());
+ });
+
+ it('should call splice sort only', () => {
+ testModule.fitFunction(dataSeries, Fit.Linear, ScaleType.Linear);
+
+ expect(dataSeries.data.sort).not.toHaveBeenCalled();
+ expect(dataSeries.data.slice).toHaveBeenCalledTimes(1);
+ expect(mockArray.sort).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not call splice.sort if sorted is true', () => {
+ testModule.fitFunction(dataSeries, Fit.Linear, ScaleType.Linear, true);
+
+ expect(dataSeries.data.slice).not.toHaveBeenCalled();
+ expect(mockArray.sort).not.toHaveBeenCalled();
+ });
+
+ it('should not call splice.sort if scale is ordinal', () => {
+ testModule.fitFunction(dataSeries, Fit.Linear, ScaleType.Ordinal);
+
+ expect(dataSeries.data.slice).not.toHaveBeenCalled();
+ expect(mockArray.sort).not.toHaveBeenCalled();
+ });
+
+ it('should call splice.sort with predicate', () => {
+ jest.spyOn(seriesUtils, 'datumXSortPredicate');
+ testModule.fitFunction(dataSeries, Fit.Linear, ScaleType.Linear);
+
+ expect(seriesUtils.datumXSortPredicate).toHaveBeenCalledWith(Fit.Linear);
+ });
+ });
+
+ describe.each([ScaleType.Linear, ScaleType.Ordinal])('ScaleType - %s', (scaleType) => {
+ const ordinal = scaleType === ScaleType.Ordinal;
+
+ beforeAll(() => {
+ dataSeries = MockDataSeries.fitFunction({ ordinal });
+ });
+
+ describe('EndValues', () => {
+ const sortedDS = MockDataSeries.fitFunction({ ordinal, shuffle: false });
+
+ describe('number value', () => {
+ const endValue = 100;
+ it('should set end values - None', () => {
+ const actual = testModule.fitFunction(sortedDS, { type: Fit.None, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toBeNull();
+ expect(finalValues[finalValues.length - 1]).toBeNull();
+ });
+
+ it('should set end values - Zero', () => {
+ const actual = testModule.fitFunction(sortedDS, { type: Fit.Zero, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toEqual(0);
+ expect(finalValues[finalValues.length - 1]).toEqual(0);
+ });
+
+ it('should set end values - Explicit', () => {
+ const actual = testModule.fitFunction(sortedDS, { type: Fit.Explicit, value: 20, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toEqual(20);
+ expect(finalValues[finalValues.length - 1]).toEqual(20);
+ });
+
+ it('should set end values - Lookahead', () => {
+ const actual = testModule.fitFunction(dataSeries, { type: Fit.Lookahead, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toEqual(3);
+ expect(finalValues[finalValues.length - 1]).toEqual(endValue);
+ });
+
+ it('should set end values - Nearest', () => {
+ const actual = testModule.fitFunction(dataSeries, { type: Fit.Nearest, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toEqual(3);
+ expect(finalValues[finalValues.length - 1]).toEqual(12);
+ });
+
+ it('should set end values - Average', () => {
+ const actual = testModule.fitFunction(dataSeries, { type: Fit.Average, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toEqual(endValue);
+ expect(finalValues[finalValues.length - 1]).toEqual(endValue);
+ });
+
+ it('should set end values - Linear', () => {
+ const actual = testModule.fitFunction(dataSeries, { type: Fit.Linear, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toEqual(endValue);
+ expect(finalValues[finalValues.length - 1]).toEqual(endValue);
+ });
+ });
+
+ describe("'nearest' value", () => {
+ const endValue = 'nearest';
+
+ it('should set end values - None', () => {
+ const actual = testModule.fitFunction(sortedDS, { type: Fit.None, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toBeNull();
+ expect(finalValues[finalValues.length - 1]).toBeNull();
+ });
+
+ it('should set end values - Zero', () => {
+ const actual = testModule.fitFunction(sortedDS, { type: Fit.Zero, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toEqual(0);
+ expect(finalValues[finalValues.length - 1]).toEqual(0);
+ });
+
+ it('should set end values - Explicit', () => {
+ const actual = testModule.fitFunction(sortedDS, { type: Fit.Explicit, value: 20, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toEqual(20);
+ expect(finalValues[finalValues.length - 1]).toEqual(20);
+ });
+
+ it('should set end values - Lookahead', () => {
+ const actual = testModule.fitFunction(dataSeries, { type: Fit.Lookahead, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toEqual(3);
+ expect(finalValues[finalValues.length - 1]).toEqual(12);
+ });
+
+ it('should set end values - Nearest', () => {
+ const actual = testModule.fitFunction(dataSeries, { type: Fit.Nearest, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toEqual(3);
+ expect(finalValues[finalValues.length - 1]).toEqual(12);
+ });
+
+ it('should set end values - Average', () => {
+ const actual = testModule.fitFunction(dataSeries, { type: Fit.Average, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toEqual(3);
+ expect(finalValues[finalValues.length - 1]).toEqual(12);
+ });
+
+ it('should set end values - Linear', () => {
+ const actual = testModule.fitFunction(dataSeries, { type: Fit.Linear, endValue }, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues[0]).toEqual(3);
+ expect(finalValues[finalValues.length - 1]).toEqual(12);
+ });
+ });
+ });
+
+ describe('Fit Types', () => {
+ describe('None', () => {
+ it('should return original dataSeries', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.None, scaleType);
+
+ expect(actual).toBe(dataSeries);
+ });
+
+ it('should return null data values without fit', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.None, scaleType);
+
+ expect(getFilledNullData(actual.data)).toEqualArrayOf(undefined, 7);
+ });
+ });
+
+ describe('Zero', () => {
+ it('should NOT return original dataSeries', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Zero, scaleType);
+
+ expect(actual).not.toBe(dataSeries);
+ });
+
+ it('should return null data values with zeros', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Zero, scaleType);
+ const testActual = getFilledNullData(actual.data);
+
+ expect(testActual).toEqualArrayOf(0, 7);
+ });
+ });
+
+ describe('Explicit', () => {
+ it('should return original dataSeries if no value provided', () => {
+ const actual = testModule.fitFunction(dataSeries, { type: Fit.Explicit }, scaleType);
+
+ expect(actual).toBe(dataSeries);
+ });
+
+ it('should return null data values with set value', () => {
+ const actual = testModule.fitFunction(dataSeries, { type: Fit.Explicit, value: 20 }, scaleType);
+ const testActual = getFilledNullData(actual.data);
+
+ expect(testActual).toEqualArrayOf(20, 7);
+ });
+ });
+
+ describe('Lookahead', () => {
+ it('should not return original dataSeries', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Lookahead, scaleType);
+
+ expect(actual).not.toBe(dataSeries);
+ });
+
+ it('should not fill non-null values', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Lookahead, scaleType);
+
+ expect(getFilledNonNullData(actual.data)).toEqualArrayOf(undefined, 6);
+ });
+
+ it('should call getValue for first datum with correct args', () => {
+ const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false });
+ testModule.fitFunction(ds, Fit.Lookahead, scaleType);
+ const [current, next] = ds.data;
+
+ expect(testModule.getValue).nthCalledWith(
+ 1,
+ expect.objectContaining(current),
+ 0,
+ null,
+ expect.objectContaining(next),
+ Fit.Lookahead,
+ undefined,
+ );
+ });
+
+ it('should call getValue for 10th (4th null) datum with correct args', () => {
+ const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false });
+ const actual = testModule.fitFunction(ds, Fit.Lookahead, scaleType);
+ const previous = actual.data[7];
+ const current = ds.data[8];
+ const next = ds.data[11];
+
+ expect(testModule.getValue).nthCalledWith(
+ 4,
+ expect.objectContaining(current),
+ 8,
+ expect.objectContaining(previous),
+ expect.objectContaining(next),
+ Fit.Lookahead,
+ undefined,
+ );
+ });
+
+ it('should call getValue for last datum with correct args', () => {
+ const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false });
+ testModule.fitFunction(ds, Fit.Lookahead, scaleType);
+ const [current, previous] = ds.data.slice().reverse();
+
+ expect(testModule.getValue).lastCalledWith(
+ expect.objectContaining(current),
+ 12,
+ expect.objectContaining(previous),
+ null,
+ Fit.Lookahead,
+ undefined,
+ );
+ });
+
+ it('should call getValue for only null values', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Lookahead, scaleType);
+ const length = getFilledNullData(actual.data).length;
+
+ expect(testModule.getValue).toBeCalledTimes(length);
+ });
+
+ it('should fill null values correctly', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Lookahead, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues).toEqual([3, 3, 5, 4, 4, 5, 5, 6, 12, 12, 12, 12, null]);
+ });
+ });
+ describe('Nearest', () => {
+ it('should not return original dataSeries', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Nearest, scaleType);
+
+ expect(actual).not.toBe(dataSeries);
+ });
+
+ it('should not fill non-null values', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Nearest, scaleType);
+
+ expect(getFilledNonNullData(actual.data)).toEqualArrayOf(undefined, 6);
+ });
+
+ it('should call getValue for first datum with correct args', () => {
+ const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false });
+ testModule.fitFunction(ds, Fit.Nearest, scaleType);
+ const [current, next] = ds.data;
+
+ expect(testModule.getValue).nthCalledWith(
+ 1,
+ expect.objectContaining(current),
+ 0,
+ null,
+ expect.objectContaining(next),
+ Fit.Nearest,
+ undefined,
+ );
+ });
+
+ it('should call getValue for 10th (4th null) datum with correct args', () => {
+ const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false });
+ const actual = testModule.fitFunction(ds, Fit.Nearest, scaleType);
+ const previous = actual.data[7];
+ const current = ds.data[8];
+ const next = ds.data[11];
+
+ expect(testModule.getValue).nthCalledWith(
+ 4,
+ expect.objectContaining(current),
+ 8,
+ expect.objectContaining(previous),
+ expect.objectContaining(next),
+ Fit.Nearest,
+ undefined,
+ );
+ });
+
+ it('should call getValue for last datum with correct args', () => {
+ const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false });
+ testModule.fitFunction(ds, Fit.Nearest, scaleType);
+ const [current, previous] = ds.data.slice().reverse();
+
+ expect(testModule.getValue).lastCalledWith(
+ expect.objectContaining(current),
+ 12,
+ expect.objectContaining(previous),
+ null,
+ Fit.Nearest,
+ undefined,
+ );
+ });
+
+ it('should call getValue for only null values', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Nearest, scaleType);
+ const length = getFilledNullData(actual.data).length;
+
+ expect(testModule.getValue).toBeCalledTimes(length);
+ });
+
+ it('should fill null values correctly', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Nearest, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues).toEqual([3, 3, 5, 5, 4, 4, 5, 6, 6, 6, 12, 12, 12]);
+ });
+ });
+
+ describe('Average', () => {
+ it('should not return original dataSeries', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Average, scaleType);
+
+ expect(actual).not.toBe(dataSeries);
+ });
+
+ it('should not fill non-null values', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Average, scaleType);
+
+ expect(getFilledNonNullData(actual.data)).toEqualArrayOf(undefined, 6);
+ });
+
+ it('should call getValue for first datum with correct args', () => {
+ const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false });
+ testModule.fitFunction(ds, Fit.Average, scaleType);
+ const [current, next] = ds.data;
+
+ expect(testModule.getValue).nthCalledWith(
+ 1,
+ expect.objectContaining(current),
+ 0,
+ null,
+ expect.objectContaining(next),
+ Fit.Average,
+ undefined,
+ );
+ });
+
+ it('should call getValue for 10th (4th null) datum with correct args', () => {
+ const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false });
+ const actual = testModule.fitFunction(ds, Fit.Average, scaleType);
+ const previous = actual.data[7];
+ const current = ds.data[8];
+ const next = ds.data[11];
+
+ expect(testModule.getValue).nthCalledWith(
+ 4,
+ expect.objectContaining(current),
+ 8,
+ expect.objectContaining(previous),
+ expect.objectContaining(next),
+ Fit.Average,
+ undefined,
+ );
+ });
+
+ it('should call getValue for last datum with correct args', () => {
+ const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false });
+ testModule.fitFunction(ds, Fit.Average, scaleType);
+ const [current, previous] = ds.data.slice().reverse();
+
+ expect(testModule.getValue).lastCalledWith(
+ expect.objectContaining(current),
+ 12,
+ expect.objectContaining(previous),
+ null,
+ Fit.Average,
+ undefined,
+ );
+ });
+
+ it('should call getValue for only null values', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Average, scaleType);
+ const length = getFilledNullData(actual.data).length;
+
+ expect(testModule.getValue).toBeCalledTimes(length);
+ });
+
+ it('should fill null values correctly', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Average, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues).toEqual([null, 3, 5, 4.5, 4, 4.5, 5, 6, 9, 9, 9, 12, null]);
+ });
+ });
+
+ describe('Linear', () => {
+ it('should not return original dataSeries', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Linear, scaleType);
+
+ expect(actual).not.toBe(dataSeries);
+ });
+
+ it('should not fill non-null values', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Linear, scaleType);
+
+ expect(getFilledNonNullData(actual.data)).toEqualArrayOf(undefined, 6);
+ });
+
+ it('should call getValue for first datum with correct args', () => {
+ const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false });
+ testModule.fitFunction(ds, Fit.Linear, scaleType);
+ const [current, next] = ds.data;
+
+ expect(testModule.getValue).nthCalledWith(
+ 1,
+ expect.objectContaining(current),
+ 0,
+ null,
+ expect.objectContaining(next),
+ Fit.Linear,
+ undefined,
+ );
+ });
+
+ it('should call getValue for 10th (4th null) datum with correct args', () => {
+ const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false });
+ const actual = testModule.fitFunction(ds, Fit.Linear, scaleType);
+ const previous = actual.data[7];
+ const current = ds.data[8];
+ const next = ds.data[11];
+
+ expect(testModule.getValue).nthCalledWith(
+ 4,
+ expect.objectContaining(current),
+ 8,
+ expect.objectContaining(previous),
+ expect.objectContaining(next),
+ Fit.Linear,
+ undefined,
+ );
+ });
+
+ it('should call getValue for last datum with correct args', () => {
+ const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false });
+ testModule.fitFunction(ds, Fit.Linear, scaleType);
+ const [current, previous] = ds.data.slice().reverse();
+
+ expect(testModule.getValue).lastCalledWith(
+ expect.objectContaining(current),
+ 12,
+ expect.objectContaining(previous),
+ null,
+ Fit.Linear,
+ undefined,
+ );
+ });
+
+ it('should call getValue for only null values', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Linear, scaleType);
+ const length = getFilledNullData(actual.data).length;
+
+ expect(testModule.getValue).toBeCalledTimes(length);
+ });
+
+ it('should fill null values correctly', () => {
+ const actual = testModule.fitFunction(dataSeries, Fit.Linear, scaleType);
+ const finalValues = getYResolvedData(actual.data);
+
+ expect(finalValues).toEqual([null, 3, 5, 4.5, 4, 4.5, 5, 6, 7.5, 9, 10.5, 12, null]);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/src/chart_types/xy_chart/utils/fit_function.ts b/src/chart_types/xy_chart/utils/fit_function.ts
new file mode 100644
index 0000000000..6e3f1bd818
--- /dev/null
+++ b/src/chart_types/xy_chart/utils/fit_function.ts
@@ -0,0 +1,230 @@
+import { DeepNonNullable } from 'utility-types';
+
+import { Fit, FitConfig } from './specs';
+import { DataSeries, DataSeriesDatum } from './series';
+import { datumXSortPredicate } from './stacked_series_utils';
+import { ScaleType } from '../../../utils/scales/scales';
+
+/**
+ * Fit type that requires previous and/or next `non-nullable` values
+ */
+export type BoundingFit = Exclude;
+
+/**
+ * `DataSeriesDatum` with non-`null` value for `x` and `y1`
+ */
+export type FullDataSeriesDatum = Omit &
+ DeepNonNullable>;
+
+/**
+ * Embellishes `FullDataSeriesDatum` with `fittingIndex` for ordinal scales
+ */
+export type WithIndex = T & { fittingIndex: number };
+
+/**
+ * Returns `[x, y1]` values for a given datum with `fittingIndex`
+ */
+export const getXYValues = ({ x, y1, fittingIndex }: WithIndex): [number, number] => {
+ return [typeof x === 'string' ? fittingIndex : x, y1];
+};
+
+export const getValue = (
+ current: DataSeriesDatum,
+ currentIndex: number,
+ previous: WithIndex | null,
+ next: WithIndex | null,
+ type: BoundingFit,
+ endValue?: number | 'nearest',
+): DataSeriesDatum => {
+ if (previous !== null && type === Fit.Carry) {
+ return {
+ ...current,
+ filled: {
+ y1: previous.y1,
+ },
+ };
+ } else if (next !== null && type === Fit.Lookahead) {
+ return {
+ ...current,
+ filled: {
+ y1: next.y1,
+ },
+ };
+ } else if (previous !== null && next !== null) {
+ if (type === Fit.Average) {
+ return {
+ ...current,
+ filled: {
+ y1: (previous.y1 + next.y1) / 2,
+ },
+ };
+ } else if (current.x !== null && previous.x !== null && next.x !== null) {
+ const [x1, y1] = getXYValues(previous);
+ const [x2, y2] = getXYValues(next);
+ const currentX = typeof current.x === 'string' ? currentIndex : current.x;
+
+ if (type === Fit.Nearest) {
+ const x1Delta = Math.abs(currentX - x1);
+ const x2Delta = Math.abs(currentX - x2);
+ return {
+ ...current,
+ filled: {
+ y1: x1Delta > x2Delta ? y2 : y1,
+ },
+ };
+ } else if (type === Fit.Linear) {
+ return {
+ ...current,
+ filled: {
+ // simple linear interpolation function
+ y1: previous.y1 + (currentX - x1) * ((y2 - y1) / (x2 - x1)),
+ },
+ };
+ }
+ }
+ } else if ((previous !== null || next !== null) && (type === Fit.Nearest || endValue === 'nearest')) {
+ return {
+ ...current,
+ filled: {
+ y1: previous !== null ? previous.y1 : next!.y1,
+ },
+ };
+ }
+
+ if (endValue === undefined || typeof endValue === 'string') {
+ return current;
+ }
+
+ // No matching fit - should only fall here on end conditions
+ return {
+ ...current,
+ filled: {
+ y1: endValue,
+ },
+ };
+};
+
+export const parseConfig = (config?: Exclude | FitConfig): FitConfig => {
+ if (!config) {
+ return {
+ type: Fit.None,
+ };
+ }
+
+ if (typeof config === 'string') {
+ return {
+ type: config,
+ };
+ }
+
+ if (config.type === Fit.Explicit && config.value === undefined) {
+ // Using explicit fit function requires a value
+ return {
+ type: Fit.None,
+ };
+ }
+
+ return {
+ type: config.type,
+ value: config.type === Fit.Explicit ? config.value : undefined,
+ endValue: config.endValue,
+ };
+};
+
+export const fitFunction = (
+ dataSeries: DataSeries,
+ fitConfig: Exclude | FitConfig,
+ xScaleType: ScaleType,
+ sorted = false,
+): DataSeries => {
+ const { type, value, endValue } = parseConfig(fitConfig);
+
+ if (type === Fit.None) {
+ return dataSeries;
+ }
+
+ const { data } = dataSeries;
+
+ if (type === Fit.Zero) {
+ return {
+ ...dataSeries,
+ data: data.map((datum) => ({
+ ...datum,
+ filled: {
+ y1: datum.y1 === null ? 0 : undefined,
+ },
+ })),
+ };
+ }
+
+ if (type === Fit.Explicit) {
+ if (value === undefined) {
+ return dataSeries;
+ }
+
+ return {
+ ...dataSeries,
+ data: data.map((datum) => ({
+ ...datum,
+ filled: {
+ y1: datum.y1 === null ? value : undefined,
+ },
+ })),
+ };
+ }
+
+ const sortedData =
+ sorted || xScaleType === ScaleType.Ordinal ? data : data.slice().sort(datumXSortPredicate(xScaleType));
+ const newData: DataSeriesDatum[] = [];
+ let previousNonNullDatum: WithIndex | null = null;
+ let nextNonNullDatum: WithIndex | null = null;
+
+ for (let i = 0; i < sortedData.length; i++) {
+ let j = i;
+ const current = sortedData[i];
+
+ if (
+ current.y1 === null &&
+ nextNonNullDatum === null &&
+ (type === Fit.Lookahead ||
+ type === Fit.Nearest ||
+ type === Fit.Average ||
+ type === Fit.Linear ||
+ endValue === 'nearest')
+ ) {
+ // Forward lookahead to get next non-null value
+ for (j = i + 1; j < sortedData.length; j++) {
+ const value = sortedData[j];
+
+ if (value.y1 !== null && value.x !== null) {
+ nextNonNullDatum = {
+ ...(value as FullDataSeriesDatum),
+ fittingIndex: j,
+ };
+ break;
+ }
+ }
+ }
+
+ const newValue =
+ current.y1 === null ? getValue(current, i, previousNonNullDatum, nextNonNullDatum, type, endValue) : current;
+
+ newData[i] = newValue;
+
+ if (current.y1 !== null && current.x !== null) {
+ previousNonNullDatum = {
+ ...(current as FullDataSeriesDatum),
+ fittingIndex: i,
+ };
+ }
+
+ if (nextNonNullDatum !== null && nextNonNullDatum.x <= current.x) {
+ nextNonNullDatum = null;
+ }
+ }
+
+ return {
+ ...dataSeries,
+ data: newData,
+ };
+};
diff --git a/src/chart_types/xy_chart/utils/interactions.ts b/src/chart_types/xy_chart/utils/interactions.ts
index 958c5aeb92..ec5ea633d8 100644
--- a/src/chart_types/xy_chart/utils/interactions.ts
+++ b/src/chart_types/xy_chart/utils/interactions.ts
@@ -1,3 +1,5 @@
+import { $Values } from 'utility-types';
+
import { BarGeometry, IndexedGeometry, isBarGeometry, isPointGeometry, PointGeometry } from '../rendering/rendering';
import { Datum, Rotation } from './specs';
import { Dimensions } from '../../../utils/dimensions';
@@ -15,11 +17,7 @@ export const TooltipType = Object.freeze({
None: 'none' as 'none',
});
-export type TooltipType =
- | typeof TooltipType.VerticalCursor
- | typeof TooltipType.Crosshairs
- | typeof TooltipType.Follow
- | typeof TooltipType.None;
+export type TooltipType = $Values;
export interface TooltipValue {
name: string;
diff --git a/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts b/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts
index 9a72963522..d45235bab8 100644
--- a/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts
+++ b/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts
@@ -1,6 +1,12 @@
import { getSpecId } from '../../../utils/ids';
-import { formatNonStackedDataSeriesValues } from './nonstacked_series_utils';
import { RawDataSeries } from './series';
+import { ScaleType } from '../../../utils/scales/scales';
+import { MockRawDataSeries, MockDataSeries } from '../../../mocks';
+import { MockSeriesSpecs, MockSeriesSpec } from '../../../mocks/specs';
+
+import * as fitFunctionModule from './fit_function';
+import * as testModule from './nonstacked_series_utils';
+import { Fit } from './specs';
const EMPTY_DATA_SET: RawDataSeries[] = [
{
@@ -172,11 +178,21 @@ const DATA_SET_WITH_NULL_2: RawDataSeries[] = [
describe('Non-Stacked Series Utils', () => {
describe('Format stacked dataset', () => {
test('empty data', () => {
- const formattedData = formatNonStackedDataSeriesValues(EMPTY_DATA_SET, false);
+ const formattedData = testModule.formatNonStackedDataSeriesValues(
+ EMPTY_DATA_SET,
+ false,
+ MockSeriesSpecs.empty(),
+ ScaleType.Linear,
+ );
expect(formattedData[0].data.length).toBe(0);
});
test('format data without nulls', () => {
- let formattedData = formatNonStackedDataSeriesValues(STANDARD_DATA_SET, false);
+ let formattedData = testModule.formatNonStackedDataSeriesValues(
+ STANDARD_DATA_SET,
+ false,
+ MockSeriesSpecs.empty(),
+ ScaleType.Linear,
+ );
expect(formattedData[0].data[0]).toEqual({
datum: undefined,
initialY0: null,
@@ -201,7 +217,12 @@ describe('Non-Stacked Series Utils', () => {
y0: 0,
y1: 30,
});
- formattedData = formatNonStackedDataSeriesValues(STANDARD_DATA_SET, true);
+ formattedData = testModule.formatNonStackedDataSeriesValues(
+ STANDARD_DATA_SET,
+ true,
+ MockSeriesSpecs.empty(),
+ ScaleType.Linear,
+ );
expect(formattedData[0].data[0]).toEqual({
datum: undefined,
initialY0: null,
@@ -228,7 +249,12 @@ describe('Non-Stacked Series Utils', () => {
});
});
test('format data with nulls', () => {
- const formattedData = formatNonStackedDataSeriesValues(WITH_NULL_DATASET, false);
+ const formattedData = testModule.formatNonStackedDataSeriesValues(
+ WITH_NULL_DATASET,
+ false,
+ MockSeriesSpecs.empty(),
+ ScaleType.Linear,
+ );
expect(formattedData[1].data[0]).toEqual({
datum: undefined,
initialY0: null,
@@ -239,7 +265,12 @@ describe('Non-Stacked Series Utils', () => {
});
});
test('format data without nulls with y0 values', () => {
- const formattedData = formatNonStackedDataSeriesValues(STANDARD_DATA_SET_WY0, false);
+ const formattedData = testModule.formatNonStackedDataSeriesValues(
+ STANDARD_DATA_SET_WY0,
+ false,
+ MockSeriesSpecs.empty(),
+ ScaleType.Linear,
+ );
expect(formattedData[0].data[0]).toEqual({
datum: undefined,
initialY0: 2,
@@ -266,7 +297,12 @@ describe('Non-Stacked Series Utils', () => {
});
});
test('format data with nulls', () => {
- const formattedData = formatNonStackedDataSeriesValues(WITH_NULL_DATASET_WY0, false);
+ const formattedData = testModule.formatNonStackedDataSeriesValues(
+ WITH_NULL_DATASET_WY0,
+ false,
+ MockSeriesSpecs.empty(),
+ ScaleType.Linear,
+ );
expect(formattedData[0].data[0]).toEqual({
datum: undefined,
initialY0: 2,
@@ -293,7 +329,12 @@ describe('Non-Stacked Series Utils', () => {
});
});
test('format data without nulls on second series', () => {
- const formattedData = formatNonStackedDataSeriesValues(DATA_SET_WITH_NULL_2, false);
+ const formattedData = testModule.formatNonStackedDataSeriesValues(
+ DATA_SET_WITH_NULL_2,
+ false,
+ MockSeriesSpecs.empty(),
+ ScaleType.Linear,
+ );
expect(formattedData.length).toBe(2);
expect(formattedData[0].data.length).toBe(3);
expect(formattedData[1].data.length).toBe(2);
@@ -340,4 +381,76 @@ describe('Non-Stacked Series Utils', () => {
});
});
});
+
+ describe('Using fit functions', () => {
+ describe.each(['area', 'line'])('Spec type - %s', (specType) => {
+ const rawDataSeries = [MockRawDataSeries.fitFunction({ shuffle: false })];
+ const dataSeries = MockDataSeries.fitFunction({ shuffle: false });
+ const spec =
+ specType === 'area' ? MockSeriesSpec.area({ fit: Fit.Linear }) : MockSeriesSpec.line({ fit: Fit.Linear });
+ const seriesSpecs = MockSeriesSpecs.fromSpecs([spec]);
+
+ beforeAll(() => {
+ jest.spyOn(fitFunctionModule, 'fitFunction').mockReturnValue(dataSeries);
+ jest.spyOn(testModule, 'formatNonStackedDataValues').mockReturnValue(dataSeries);
+ });
+
+ it('return call formatNonStackedDataValues with args', () => {
+ testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, seriesSpecs, ScaleType.Linear);
+
+ expect(testModule.formatNonStackedDataValues).toHaveBeenCalledWith(rawDataSeries[0], false);
+ });
+
+ it('return call fitFunction with args', () => {
+ testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, seriesSpecs, ScaleType.Linear);
+
+ expect(fitFunctionModule.fitFunction).toHaveBeenCalledWith(dataSeries, Fit.Linear, ScaleType.Linear);
+ });
+
+ it('return not call fitFunction if no fit specified', () => {
+ const spec =
+ specType === 'area' ? MockSeriesSpec.area({ fit: undefined }) : MockSeriesSpec.line({ fit: undefined });
+ const noFitSpec = MockSeriesSpecs.fromSpecs([spec]);
+ testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, noFitSpec, ScaleType.Linear);
+
+ expect(fitFunctionModule.fitFunction).not.toHaveBeenCalled();
+ });
+
+ it('return fitted dataSeries', () => {
+ const actual = testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, seriesSpecs, ScaleType.Linear);
+
+ expect(actual[0]).toBe(dataSeries);
+ });
+ });
+
+ describe('Non area and line specs', () => {
+ const rawDataSeries = [MockRawDataSeries.fitFunction({ shuffle: false })];
+ const dataSeries = MockDataSeries.fitFunction({ shuffle: false });
+ const spec = MockSeriesSpec.bar();
+ const seriesSpecs = MockSeriesSpecs.fromSpecs([spec]);
+
+ beforeAll(() => {
+ jest.spyOn(fitFunctionModule, 'fitFunction').mockReturnValue(dataSeries);
+ jest.spyOn(testModule, 'formatNonStackedDataValues').mockReturnValue(dataSeries);
+ });
+
+ it('return call formatNonStackedDataValues with args', () => {
+ testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, seriesSpecs, ScaleType.Linear);
+
+ expect(testModule.formatNonStackedDataValues).toHaveBeenCalledWith(rawDataSeries[0], false);
+ });
+
+ it('return call fitFunction with args', () => {
+ testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, seriesSpecs, ScaleType.Linear);
+
+ expect(fitFunctionModule.fitFunction).not.toHaveBeenCalled();
+ });
+
+ it('return fitted dataSeries', () => {
+ const actual = testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, seriesSpecs, ScaleType.Linear);
+
+ expect(actual[0]).toBe(dataSeries);
+ });
+ });
+ });
});
diff --git a/src/chart_types/xy_chart/utils/nonstacked_series_utils.ts b/src/chart_types/xy_chart/utils/nonstacked_series_utils.ts
index cd767c013c..6b30fa3004 100644
--- a/src/chart_types/xy_chart/utils/nonstacked_series_utils.ts
+++ b/src/chart_types/xy_chart/utils/nonstacked_series_utils.ts
@@ -1,26 +1,44 @@
import { DataSeries, DataSeriesDatum, RawDataSeries } from './series';
+import { fitFunction } from './fit_function';
+import { isAreaSeriesSpec, isLineSeriesSpec, SeriesSpecs } from './specs';
+import { ScaleType } from '../../../utils/scales/scales';
-export function formatNonStackedDataSeriesValues(dataseries: RawDataSeries[], scaleToExtent: boolean): DataSeries[] {
+export const formatNonStackedDataSeriesValues = (
+ dataseries: RawDataSeries[],
+ scaleToExtent: boolean,
+ seriesSpecs: SeriesSpecs,
+ xScaleType: ScaleType,
+): DataSeries[] => {
const len = dataseries.length;
- let i;
const formattedValues: DataSeries[] = [];
- for (i = 0; i < len; i++) {
- const formattedValue = formatNonStackedDataValues(dataseries[i], scaleToExtent);
- formattedValues.push(formattedValue);
+ for (let i = 0; i < len; i++) {
+ const formattedDataValue = formatNonStackedDataValues(dataseries[i], scaleToExtent);
+ const spec = seriesSpecs.get(formattedDataValue.specId);
+
+ if (
+ spec !== null &&
+ spec !== undefined &&
+ (isAreaSeriesSpec(spec) || isLineSeriesSpec(spec)) &&
+ spec.fit !== undefined
+ ) {
+ const fittedData = fitFunction(formattedDataValue, spec.fit, xScaleType);
+ formattedValues.push(fittedData);
+ } else {
+ formattedValues.push(formattedDataValue);
+ }
}
return formattedValues;
-}
+};
-export function formatNonStackedDataValues(dataSeries: RawDataSeries, scaleToExtent: boolean): DataSeries {
+export const formatNonStackedDataValues = (dataSeries: RawDataSeries, scaleToExtent: boolean): DataSeries => {
const len = dataSeries.data.length;
- let i;
const formattedValues: DataSeries = {
key: dataSeries.key,
specId: dataSeries.specId,
seriesColorKey: dataSeries.seriesColorKey,
data: [],
};
- for (i = 0; i < len; i++) {
+ for (let i = 0; i < len; i++) {
const data = dataSeries.data[i];
const { x, y1, datum } = data;
let y0: number | null;
@@ -45,4 +63,4 @@ export function formatNonStackedDataValues(dataSeries: RawDataSeries, scaleToExt
formattedValues.data.push(formattedValue);
}
return formattedValues;
-}
+};
diff --git a/src/chart_types/xy_chart/utils/series.test.ts b/src/chart_types/xy_chart/utils/series.test.ts
index 9e6787ef1f..ba284c5540 100644
--- a/src/chart_types/xy_chart/utils/series.test.ts
+++ b/src/chart_types/xy_chart/utils/series.test.ts
@@ -360,6 +360,7 @@ describe('Series', () => {
splittedDataSeries.splittedSeries,
xValues,
ScaleType.Linear,
+ new Map(),
);
expect(stackedDataSeries.stacked).toMatchSnapshot();
});
diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts
index 3627244ea7..9a68fde443 100644
--- a/src/chart_types/xy_chart/utils/series.ts
+++ b/src/chart_types/xy_chart/utils/series.ts
@@ -11,14 +11,14 @@ import { ScaleType } from '../../../utils/scales/scales';
export interface FilledValues {
/** the x value */
- x: number | string;
+ x?: number | string;
/** the max y value */
- y1: number | null;
+ y1?: number;
/** the minimum y value */
- y0: number | null;
+ y0?: number;
}
-export interface RawDataSeriesDatum {
+export interface RawDataSeriesDatum {
/** the x value */
x: number | string;
/** the main y metric */
@@ -26,10 +26,10 @@ export interface RawDataSeriesDatum {
/** the optional y0 metric, used for bars or area with a lower bound */
y0?: number | null;
/** the datum */
- datum?: any;
+ datum?: T;
}
-export interface DataSeriesDatum {
+export interface DataSeriesDatum {
/** the x value */
x: number | string;
/** the max y value */
@@ -40,10 +40,10 @@ export interface DataSeriesDatum {
initialY1: number | null;
/** initial y0 value, non stacked */
initialY0: number | null;
- /** the datum */
- datum?: any;
+ /** initial datum */
+ datum?: T;
/** the list of filled values because missing or nulls */
- filled?: Partial;
+ filled?: FilledValues;
}
export interface DataSeries {
@@ -218,6 +218,7 @@ export function getFormattedDataseries(
dataSeries: Map,
xValues: Set,
xScaleType: ScaleType,
+ seriesSpecs: Map,
): {
stacked: FormattedDataSeries[];
nonStacked: FormattedDataSeries[];
@@ -258,7 +259,7 @@ export function getFormattedDataseries(
nonStackedFormattedDataSeries.push({
groupId,
counts: nonStackedDataSeries.counts,
- dataSeries: formatNonStackedDataSeriesValues(nonStackedDataSeries.rawDataSeries, false),
+ dataSeries: formatNonStackedDataSeriesValues(nonStackedDataSeries.rawDataSeries, false, seriesSpecs, xScaleType),
});
});
return {
diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts
index db02c886d6..999539b653 100644
--- a/src/chart_types/xy_chart/utils/specs.ts
+++ b/src/chart_types/xy_chart/utils/specs.ts
@@ -8,13 +8,14 @@ import {
PointStyle,
} from '../../../utils/themes/theme';
import { Accessor, AccessorFormat } from '../../../utils/accessor';
-import { Omit, RecursivePartial } from '../../../utils/commons';
+import { RecursivePartial } from '../../../utils/commons';
import { AnnotationId, AxisId, GroupId, SpecId } from '../../../utils/ids';
import { ScaleContinuousType, ScaleType } from '../../../utils/scales/scales';
import { CurveType } from '../../../utils/curves';
import { DataSeriesColorsValues, RawDataSeriesDatum } from './series';
import { GeometryId } from '../rendering/rendering';
import { AnnotationTooltipFormatter } from '../annotations/annotation_utils';
+import { $Values } from 'utility-types';
export type Datum = any;
export type Rotation = 0 | 90 | -90 | 180;
@@ -54,6 +55,90 @@ interface DomainMinInterval {
minInterval?: number;
}
+/**
+ * The fit function type
+ */
+export const Fit = Object.freeze({
+ /**
+ * Don't draw value on the graph. Slices out area between `null` values.
+ *
+ * Example:
+ * ```js
+ * [2, null, null, 8] => [2, null null, 8]
+ * ```
+ */
+ None: 'none' as 'none',
+ /**
+ * Use the previous non-`null` value
+ *
+ * Example:
+ * ```js
+ * [2, null, null, 8] => [2, 2, 2, 8]
+ * ```
+ *
+ * @opposite `Lookahead`
+ */
+ Carry: 'carry' as 'carry',
+ /**
+ * Use the next non-`null` value
+ *
+ * Example:
+ * ```js
+ * [2, null, null, 8] => [2, 8, 8, 8]
+ * ```
+ *
+ * @opposite `Carry`
+ */
+ Lookahead: 'lookahead' as 'lookahead',
+ /**
+ * Use the closest non-`null` value (before or after)
+ *
+ * Example:
+ * ```js
+ * [2, null, null, 8] => [2, 2, 8, 8]
+ * ```
+ */
+ Nearest: 'nearest' as 'nearest',
+ /**
+ * Average between the closest non-`null` values
+ *
+ * Example:
+ * ```js
+ * [2, null, null, 8] => [2, 5, 5, 8]
+ * ```
+ */
+ Average: 'average' as 'average',
+ /**
+ * Linear interpolation between the closest non-`null` values
+ *
+ * Example:
+ * ```js
+ * [2, null, null, 8] => [2, 4, 6, 8]
+ * ```
+ */
+ Linear: 'linear' as 'linear',
+ /**
+ * Sets all `null` values to `0`
+ *
+ * Example:
+ * ```js
+ * [2, null, null, 8] => [2, 0, 0, 8]
+ * ```
+ */
+ Zero: 'zero' as 'zero',
+ /**
+ * Specify an explicit value `X`
+ *
+ * Example:
+ * ```js
+ * [2, null, null, 8] => [2, X, X, 8]
+ * ```
+ */
+ Explicit: 'explicit' as 'explicit',
+});
+
+export type Fit = $Values;
+
interface LowerBound {
/** Lower bound of domain range */
min: number;
@@ -179,6 +264,8 @@ export interface SeriesScales {
export type BasicSeriesSpec = SeriesSpec & SeriesAccessors & SeriesScales;
+export type SeriesSpecs = Map;
+
/**
* This spec describe the dataset configuration used to display a bar series.
*/
@@ -215,6 +302,25 @@ export type HistogramBarSeriesSpec = Omit & {
enableHistogramMode: true;
};
+export type FitConfig = {
+ /**
+ * Fit type for data with null values
+ */
+ type: Fit;
+ /**
+ * Fit value used when `type` is set to `Fit.Explicit`
+ */
+ value?: number;
+ /**
+ * Value used for first and last point if fitting is not possible
+ *
+ * `'nearest'` will set indeterminate end values to the closes _visible_ point.
+ *
+ * Note: Computed fit values will always take precedence over `endValues`
+ */
+ endValue?: number | 'nearest';
+};
+
/**
* This spec describe the dataset configuration used to display a line series.
*/
@@ -228,6 +334,10 @@ export type LineSeriesSpec = BasicSeriesSpec &
* An optional functional accessor to return custom color or style for point datum
*/
pointStyleAccessor?: PointStyleAccessor;
+ /**
+ * Fit config to fill `null` values in dataset
+ */
+ fit?: Exclude | FitConfig;
};
/**
@@ -249,6 +359,10 @@ export type AreaSeriesSpec = BasicSeriesSpec &
* An optional functional accessor to return custom color or style for point datum
*/
pointStyleAccessor?: PointStyleAccessor;
+ /**
+ * Fit config to fill `null` values in dataset
+ */
+ fit?: Exclude | FitConfig;
};
interface HistogramConfig {
@@ -330,7 +444,7 @@ export const Position = Object.freeze({
Right: 'right' as 'right',
});
-export type Position = typeof Position.Top | typeof Position.Bottom | typeof Position.Left | typeof Position.Right;
+export type Position = $Values;
export const AnnotationTypes = Object.freeze({
Line: 'line' as 'line',
@@ -338,17 +452,14 @@ export const AnnotationTypes = Object.freeze({
Text: 'text' as 'text',
});
-export type AnnotationType =
- | typeof AnnotationTypes.Line
- | typeof AnnotationTypes.Rectangle
- | typeof AnnotationTypes.Text;
+export type AnnotationType = $Values;
export const AnnotationDomainTypes = Object.freeze({
XDomain: 'xDomain' as 'xDomain',
YDomain: 'yDomain' as 'yDomain',
});
-export type AnnotationDomainType = typeof AnnotationDomainTypes.XDomain | typeof AnnotationDomainTypes.YDomain;
+export type AnnotationDomainType = $Values;
export interface LineAnnotationDatum {
dataValue: any;
diff --git a/src/chart_types/xy_chart/utils/stacked_series_utils.ts b/src/chart_types/xy_chart/utils/stacked_series_utils.ts
index e021ce791c..53d5b5bb33 100644
--- a/src/chart_types/xy_chart/utils/stacked_series_utils.ts
+++ b/src/chart_types/xy_chart/utils/stacked_series_utils.ts
@@ -7,6 +7,13 @@ interface StackedValues {
total: number;
}
+export const datumXSortPredicate = (xScaleType: ScaleType) => (a: DataSeriesDatum, b: DataSeriesDatum) => {
+ if (xScaleType === ScaleType.Ordinal || typeof a.x === 'string' || typeof b.x === 'string') {
+ return 0;
+ }
+ return a.x - b.x;
+};
+
/**
* Map each y value from a RawDataSeries on it's specific x value into,
* ordering the stack based on the dataseries index.
@@ -134,12 +141,7 @@ export function formatStackedDataSeriesValues(
newData.push(filledSeriesDatum);
}
}
- newData.sort((a, b) => {
- if (xScaleType === ScaleType.Ordinal || typeof a.x === 'string' || typeof b.x === 'string') {
- return 0;
- }
- return a.x - b.x;
- });
+ newData.sort(datumXSortPredicate(xScaleType));
return {
specId: ds.specId,
key: ds.key,
@@ -157,7 +159,7 @@ function getStackedFormattedSeriesDatum(
seriesIndex: number,
scaleToExtent: boolean,
isPercentageMode = false,
- filled?: Partial,
+ filled?: FilledValues,
): DataSeriesDatum | undefined {
const { x, datum } = data;
const stack = stackedValues.get(x);
@@ -200,7 +202,7 @@ function getStackedFormattedSeriesDatum(
stackedY1 = y1 !== null ? stackY + y1 : null;
stackedY0 = y0 != null ? stackY + y0 : stackY;
// configure null y0 if y1 is null
- // it's semantically right to say y0 is null if y1 is null
+ // it's semantically correct to say y0 is null if y1 is null
if (stackedY1 === null) {
stackedY0 = null;
}
diff --git a/src/components/icons/icon.tsx b/src/components/icons/icon.tsx
index fddea92545..66b2686bc0 100644
--- a/src/components/icons/icon.tsx
+++ b/src/components/icons/icon.tsx
@@ -8,8 +8,6 @@ import { EyeClosedIcon } from './assets/eye_closed';
import { ListIcon } from './assets/list';
import { QuestionInCircle } from './assets/question_in_circle';
-export type Omit = Pick>;
-
const typeToIconMap = {
alert: AlertIcon,
dot: DotIcon,
diff --git a/src/components/react_canvas/area_geometries.tsx b/src/components/react_canvas/area_geometries.tsx
index 618c3a206f..0ae752443c 100644
--- a/src/components/react_canvas/area_geometries.tsx
+++ b/src/components/react_canvas/area_geometries.tsx
@@ -1,6 +1,7 @@
-import { Group as KonvaGroup, ContainerConfig } from 'konva';
import React from 'react';
+import { Group as KonvaGroup, PathConfig } from 'konva';
import { Circle, Group, Path } from 'react-konva';
+
import { LegendItem } from '../../chart_types/xy_chart/legend/legend';
import {
AreaGeometry,
@@ -16,6 +17,8 @@ import {
buildPointRenderProps,
PointStyleProps,
buildLineRenderProps,
+ Clippings,
+ clipRanges,
} from './utils/rendering_props_utils';
import { mergePartial } from '../../utils/commons';
@@ -24,7 +27,7 @@ interface AreaGeometriesDataProps {
areas: AreaGeometry[];
sharedStyle: SharedGeometryStateStyle;
highlightedLegendItem: LegendItem | null;
- clippings: ContainerConfig;
+ clippings: Clippings;
}
interface AreaGeometriesDataState {
overPoint?: PointGeometry;
@@ -70,12 +73,26 @@ export class AreaGeometries extends React.PureComponent {
- const { area, color, transform, geometryId, seriesAreaStyle } = glyph;
+ const { area, color, transform, geometryId, seriesAreaStyle, clippedRanges } = glyph;
const geometryStateStyle = getGeometryStateStyle(geometryId, highlightedLegendItem, sharedStyle);
const key = getGeometryIdKey(geometryId, 'area-');
const areaProps = buildAreaRenderProps(transform.x, area, color, seriesAreaStyle, geometryStateStyle);
+
+ if (clippedRanges.length > 0) {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
return (
@@ -87,19 +104,39 @@ export class AreaGeometries extends React.PureComponent {
- const { lines, color, geometryId, transform, seriesAreaLineStyle } = glyph;
+ const { lines, color, geometryId, transform, seriesAreaLineStyle, clippedRanges } = glyph;
const geometryStateStyle = getGeometryStateStyle(geometryId, highlightedLegendItem, sharedStyle);
const groupKey = getGeometryIdKey(geometryId, `area-line-${areaIndex}`);
- const linesElements = lines.map((linePath, lineIndex) => {
+ const linesElementProps = lines.map<{ key: string; props: PathConfig }>((linePath, lineIndex) => {
const key = getGeometryIdKey(geometryId, `area-line-${areaIndex}-${lineIndex}`);
- const lineProps = buildLineRenderProps(transform.x, linePath, color, seriesAreaLineStyle, geometryStateStyle);
- return ;
+ const props = buildLineRenderProps(transform.x, linePath, color, seriesAreaLineStyle, geometryStateStyle);
+ return { key, props };
});
+
+ if (clippedRanges.length > 0) {
+ return (
+
+
+ {linesElementProps.map(({ key, props }) => (
+
+ ))}
+
+
+ {linesElementProps.map(({ key, props }) => (
+
+ ))}
+
+
+ );
+ }
+
return (
- {...linesElements}
+ {linesElementProps.map(({ key, props }) => (
+
+ ))}
);
};
diff --git a/src/components/react_canvas/bar_geometries.tsx b/src/components/react_canvas/bar_geometries.tsx
index e7927313a8..646decef6b 100644
--- a/src/components/react_canvas/bar_geometries.tsx
+++ b/src/components/react_canvas/bar_geometries.tsx
@@ -1,18 +1,18 @@
-import { Group as KonvaGroup, ContainerConfig } from 'konva';
+import { Group as KonvaGroup } from 'konva';
import React from 'react';
import { Group, Rect } from 'react-konva';
import { animated, Spring } from 'react-spring/renderprops-konva.cjs';
import { LegendItem } from '../../chart_types/xy_chart/legend/legend';
import { BarGeometry, getGeometryStateStyle } from '../../chart_types/xy_chart/rendering/rendering';
import { SharedGeometryStateStyle } from '../../utils/themes/theme';
-import { buildBarRenderProps, buildBarBorderRenderProps } from './utils/rendering_props_utils';
+import { buildBarRenderProps, buildBarBorderRenderProps, Clippings } from './utils/rendering_props_utils';
interface BarGeometriesDataProps {
animated?: boolean;
bars: BarGeometry[];
sharedStyle: SharedGeometryStateStyle;
highlightedLegendItem: LegendItem | null;
- clippings: ContainerConfig;
+ clippings: Clippings;
}
interface BarGeometriesDataState {
overBar?: BarGeometry;
diff --git a/src/components/react_canvas/line_geometries.tsx b/src/components/react_canvas/line_geometries.tsx
index 4ee5183b89..20799e5207 100644
--- a/src/components/react_canvas/line_geometries.tsx
+++ b/src/components/react_canvas/line_geometries.tsx
@@ -1,4 +1,4 @@
-import { Group as KonvaGroup, ContainerConfig } from 'konva';
+import { Group as KonvaGroup } from 'konva';
import React from 'react';
import { Circle, Group, Path } from 'react-konva';
import { LegendItem } from '../../chart_types/xy_chart/legend/legend';
@@ -14,6 +14,8 @@ import {
buildPointStyleProps,
PointStyleProps,
buildPointRenderProps,
+ Clippings,
+ clipRanges,
} from './utils/rendering_props_utils';
import { mergePartial } from '../../utils/commons';
@@ -22,7 +24,7 @@ interface LineGeometriesDataProps {
lines: LineGeometry[];
sharedStyle: SharedGeometryStateStyle;
highlightedLegendItem: LegendItem | null;
- clippings: ContainerConfig;
+ clippings: Clippings;
}
interface LineGeometriesDataState {
overPoint?: PointGeometry;
@@ -92,9 +94,23 @@ export class LineGeometries extends React.PureComponent 0) {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
return (
diff --git a/src/components/react_canvas/reactive_chart.tsx b/src/components/react_canvas/reactive_chart.tsx
index 988f109034..abe277b18e 100644
--- a/src/components/react_canvas/reactive_chart.tsx
+++ b/src/components/react_canvas/reactive_chart.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { inject, observer } from 'mobx-react';
-import { ContainerConfig } from 'konva';
import { Layer, Rect, Stage } from 'react-konva';
+
import { AnnotationId } from '../../utils/ids';
import { isLineAnnotation, isRectAnnotation, AxisSpec } from '../../chart_types/xy_chart/utils/specs';
import { LineAnnotationStyle, RectAnnotationStyle, mergeGridLineConfigs } from '../../utils/themes/theme';
@@ -22,6 +22,7 @@ import { LineGeometries } from './line_geometries';
import { RectAnnotation } from './rect_annotation';
import { AxisTick, AxisTicksDimensions, isVerticalGrid } from '../../chart_types/xy_chart/utils/axis_utils';
import { Dimensions } from '../../utils/dimensions';
+import { Clippings } from './utils/rendering_props_utils';
interface ReactiveChartProps {
chartStore?: ChartStore; // FIX until we find a better way on ts mobx
@@ -88,7 +89,7 @@ class Chart extends React.Component {
window.removeEventListener('mouseup', this.onEndBrushing);
}
- renderBarSeries = (clippings: ContainerConfig): ReactiveChartElementIndex[] => {
+ renderBarSeries = (clippings: Clippings): ReactiveChartElementIndex[] => {
const { geometries, canDataBeAnimated, chartTheme } = this.props.chartStore!;
if (!geometries) {
return [];
@@ -113,7 +114,7 @@ class Chart extends React.Component {
},
];
};
- renderLineSeries = (clippings: ContainerConfig): ReactiveChartElementIndex[] => {
+ renderLineSeries = (clippings: Clippings): ReactiveChartElementIndex[] => {
const { geometries, canDataBeAnimated, chartTheme } = this.props.chartStore!;
if (!geometries) {
return [];
@@ -139,7 +140,7 @@ class Chart extends React.Component {
},
];
};
- renderAreaSeries = (clippings: ContainerConfig): ReactiveChartElementIndex[] => {
+ renderAreaSeries = (clippings: Clippings): ReactiveChartElementIndex[] => {
const { geometries, canDataBeAnimated, chartTheme } = this.props.chartStore!;
if (!geometries) {
return [];
@@ -352,11 +353,12 @@ class Chart extends React.Component {
sortAndRenderElements() {
const { chartRotation, chartDimensions } = this.props.chartStore!;
+ const { height, width } = chartDimensions;
const clippings = {
clipX: 0,
clipY: 0,
- clipWidth: [90, -90].includes(chartRotation) ? chartDimensions.height : chartDimensions.width,
- clipHeight: [90, -90].includes(chartRotation) ? chartDimensions.width : chartDimensions.height,
+ clipWidth: [90, -90].includes(chartRotation) ? height : width,
+ clipHeight: [90, -90].includes(chartRotation) ? width : height,
};
const bars = this.renderBarSeries(clippings);
diff --git a/src/components/react_canvas/utils/rendering_props_utils.test.ts b/src/components/react_canvas/utils/rendering_props_utils.test.ts
index e5fe6e8a06..581fab3cd7 100644
--- a/src/components/react_canvas/utils/rendering_props_utils.test.ts
+++ b/src/components/react_canvas/utils/rendering_props_utils.test.ts
@@ -10,8 +10,12 @@ import {
rotateBarValueProps,
buildPointStyleProps,
buildBarBorderRenderProps,
+ Clippings,
+ clipRanges,
} from './rendering_props_utils';
import { RectBorderStyle, RectStyle } from '../../../utils/themes/theme';
+import { forcedType } from '../../../mocks/utils';
+import { ClippedRanges } from '../../../chart_types/xy_chart/rendering/rendering';
describe('[canvas] Area Geometries props', () => {
test('can build area point props', () => {
@@ -977,4 +981,100 @@ describe('[canvas] Bar Geometries', () => {
expect(props).toBeNull();
});
});
+
+ describe('clipRanges', () => {
+ const clippedRanges: ClippedRanges = [[0, 1], [2, 4], [4, 6], [7, 11], [11, 12]];
+ const singleRange: ClippedRanges = [[0, 1]];
+ const clippings: Clippings = {
+ clipHeight: 111,
+ clipWidth: 222,
+ };
+ const mockCtx = forcedType({
+ rect: jest.fn(),
+ });
+
+ describe('clipping is NOT negated', () => {
+ it('should call ctx with correct args - empty range', () => {
+ clipRanges([], clippings, false)(mockCtx);
+
+ expect(mockCtx.rect).not.toBeCalled();
+ });
+
+ describe('length equal to 1', () => {
+ it('should call ctx with correct args for start range - single range', () => {
+ clipRanges(singleRange, clippings, false)(mockCtx);
+
+ expect(mockCtx.rect).toHaveBeenNthCalledWith(1, 0, 0, singleRange[0][0], clippings.clipHeight);
+ });
+
+ it('should call ctx with correct args for end range - single range', () => {
+ clipRanges(singleRange, clippings, false)(mockCtx);
+ const lastX = singleRange[singleRange.length - 1][1];
+
+ expect(mockCtx.rect).toHaveBeenNthCalledWith(2, lastX, 0, clippings.clipWidth - lastX, clippings.clipHeight);
+ });
+
+ it('should only call ctx twice', () => {
+ clipRanges(singleRange, clippings, false)(mockCtx);
+
+ expect(mockCtx.rect).toBeCalledTimes(2);
+ });
+ });
+
+ describe('length greater than 1', () => {
+ it('should call ctx with correct args for start range - single range', () => {
+ clipRanges(clippedRanges, clippings, false)(mockCtx);
+
+ expect(mockCtx.rect).toHaveBeenNthCalledWith(1, 0, 0, clippedRanges[0][0], clippings.clipHeight);
+ });
+
+ it('should call ctx with correct args for end range - single range', () => {
+ clipRanges(clippedRanges, clippings, false)(mockCtx);
+ const lastX = clippedRanges[clippedRanges.length - 1][1];
+
+ expect(mockCtx.rect).toHaveBeenNthCalledWith(2, lastX, 0, clippings.clipWidth - lastX, clippings.clipHeight);
+ });
+
+ it('should call ctx with correct args', () => {
+ clipRanges(clippedRanges, clippings, false)(mockCtx);
+
+ for (let i = 1; i < length; i++) {
+ const [, x0] = clippedRanges[i - 1];
+ const [x1] = clippedRanges[i];
+
+ expect(mockCtx.rect).toHaveBeenNthCalledWith(i + 3, x0, 0, x1 - x0, clippings.clipHeight);
+ }
+ });
+
+ it('should only call ctx for (n - 1) range plus 2 for ends', () => {
+ clipRanges(clippedRanges, clippings, false)(mockCtx);
+
+ expect(mockCtx.rect).toBeCalledTimes(clippedRanges.length - 1 + 2);
+ });
+ });
+ });
+
+ describe('clipping is negated', () => {
+ it('should call ctx with correct args - empty range', () => {
+ clipRanges([], clippings, true)(mockCtx);
+
+ expect(mockCtx.rect).not.toBeCalled();
+ });
+
+ it('should call ctx with correct args - single range', () => {
+ clipRanges(singleRange, clippings, true)(mockCtx);
+ const [x0, x1] = clippedRanges[0];
+
+ expect(mockCtx.rect).toHaveBeenNthCalledWith(1, x0, 0, x1 - x0, clippings.clipHeight);
+ });
+
+ it('should call ctx with correct args', () => {
+ clipRanges(clippedRanges, clippings, true)(mockCtx);
+
+ clippedRanges.forEach(([x0, x1], i) => {
+ expect(mockCtx.rect).toHaveBeenNthCalledWith(i + 1, x0, 0, x1 - x0, clippings.clipHeight);
+ });
+ });
+ });
+ });
});
diff --git a/src/components/react_canvas/utils/rendering_props_utils.ts b/src/components/react_canvas/utils/rendering_props_utils.ts
index d6994218c3..0c320da9d2 100644
--- a/src/components/react_canvas/utils/rendering_props_utils.ts
+++ b/src/components/react_canvas/utils/rendering_props_utils.ts
@@ -1,3 +1,7 @@
+import { RectConfig, PathConfig, CircleConfig, ContainerConfig } from 'konva';
+import { Required } from 'utility-types';
+
+import { ClippedRanges } from '../../../chart_types/xy_chart/rendering/rendering';
import { Rotation } from '../../../chart_types/xy_chart/utils/specs';
import {
AreaStyle,
@@ -10,7 +14,6 @@ import {
} from '../../../utils/themes/theme';
import { Dimensions } from '../../../utils/dimensions';
import { GlobalKonvaElementProps } from '../globals';
-import { RectConfig, PathConfig, CircleConfig } from 'konva';
export interface PointStyleProps {
radius: number;
@@ -21,6 +24,8 @@ export interface PointStyleProps {
opacity: number;
}
+export type Clippings = Required;
+
export function rotateBarValueProps(
chartRotation: Rotation,
chartDimensions: Dimensions,
@@ -416,3 +421,44 @@ export function buildBarBorderRenderProps(
...GlobalKonvaElementProps,
};
}
+
+/**
+ * Creates `clipFunc` for Konva paths that have clipped ranges
+ *
+ * @param clippedRanges ranges to be clipped from rendering
+ * @param clippings konva global clippings
+ * @param negate show, rather than exclude, only selected ranges
+ */
+export function clipRanges(
+ clippedRanges: ClippedRanges,
+ clippings: Clippings,
+ negate = false,
+): (ctx: CanvasRenderingContext2D) => void {
+ const length = clippedRanges.length;
+ const { clipHeight, clipWidth } = clippings;
+
+ if (negate) {
+ return (ctx) => {
+ clippedRanges.forEach(([x0, x1]) => {
+ ctx.rect(x0, 0, x1 - x0, clippings.clipHeight);
+ });
+ };
+ }
+
+ return (ctx) => {
+ if (length > 0) {
+ ctx.rect(0, 0, clippedRanges[0][0], clipHeight);
+ const lastX = clippedRanges[length - 1][1];
+ ctx.rect(lastX, 0, clipWidth - lastX, clipHeight);
+ }
+
+ if (length > 1) {
+ for (let i = 1; i < length; i++) {
+ const [, x0] = clippedRanges[i - 1];
+ const [x1] = clippedRanges[i];
+
+ ctx.rect(x0, 0, x1 - x0, clipHeight);
+ }
+ }
+ };
+}
diff --git a/src/mocks/index.ts b/src/mocks/index.ts
new file mode 100644
index 0000000000..0ca60a9092
--- /dev/null
+++ b/src/mocks/index.ts
@@ -0,0 +1 @@
+export * from './series';
diff --git a/src/mocks/models.ts b/src/mocks/models.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/mocks/scale/index.ts b/src/mocks/scale/index.ts
new file mode 100644
index 0000000000..44544b730a
--- /dev/null
+++ b/src/mocks/scale/index.ts
@@ -0,0 +1 @@
+export * from './scale';
diff --git a/src/mocks/scale/scale.ts b/src/mocks/scale/scale.ts
new file mode 100644
index 0000000000..53384e29f1
--- /dev/null
+++ b/src/mocks/scale/scale.ts
@@ -0,0 +1,25 @@
+import { mergePartial } from '../../utils/commons';
+import { Scale, ScaleType } from '../../utils/scales/scales';
+
+export class MockScale {
+ private static readonly base: Scale = {
+ scale: jest.fn().mockImplementation((x) => x),
+ type: ScaleType.Linear,
+ bandwidth: 0,
+ minInterval: 0,
+ barsPadding: 0,
+ range: [0, 100],
+ domain: [0, 100],
+ ticks: jest.fn(),
+ pureScale: jest.fn(),
+ invert: jest.fn(),
+ invertWithStep: jest.fn(),
+ isSingleValue: jest.fn(),
+ isValueInDomain: jest.fn(),
+ isInverted: false,
+ };
+
+ static default(partial: Partial): Scale {
+ return mergePartial(MockScale.base, partial);
+ }
+}
diff --git a/src/mocks/series/data.ts b/src/mocks/series/data.ts
new file mode 100644
index 0000000000..2718eb98e8
--- /dev/null
+++ b/src/mocks/series/data.ts
@@ -0,0 +1,147 @@
+import { DataSeriesDatum } from '../../chart_types/xy_chart/utils/series';
+
+export const fitFunctionData: DataSeriesDatum[] = [
+ {
+ x: 0,
+ y1: null,
+ y0: null,
+ initialY1: null,
+ initialY0: null,
+ datum: {
+ x: 0,
+ y: null,
+ },
+ },
+ {
+ x: 1,
+ y1: 3,
+ y0: 0,
+ initialY1: 3,
+ initialY0: null,
+ datum: {
+ x: 1,
+ y: 3,
+ },
+ },
+ {
+ x: 2,
+ y1: 5,
+ y0: 0,
+ initialY1: 5,
+ initialY0: null,
+ datum: {
+ x: 2,
+ y: 5,
+ },
+ },
+ {
+ x: 3,
+ y1: null,
+ y0: null,
+ initialY1: null,
+ initialY0: null,
+ datum: {
+ x: 3,
+ y: null,
+ },
+ },
+ {
+ x: 4,
+ y1: 4,
+ y0: 0,
+ initialY1: 4,
+ initialY0: null,
+ datum: {
+ x: 4,
+ y: 4,
+ },
+ },
+ {
+ x: 5,
+ y1: null,
+ y0: null,
+ initialY1: null,
+ initialY0: null,
+ datum: {
+ x: 5,
+ y: null,
+ },
+ },
+ {
+ x: 6,
+ y1: 5,
+ y0: 0,
+ initialY1: 5,
+ initialY0: null,
+ datum: {
+ x: 6,
+ y: 5,
+ },
+ },
+ {
+ x: 7,
+ y1: 6,
+ y0: 0,
+ initialY1: 6,
+ initialY0: null,
+ datum: {
+ x: 7,
+ y: 6,
+ },
+ },
+ {
+ x: 8,
+ y1: null,
+ y0: null,
+ initialY1: null,
+ initialY0: null,
+ datum: {
+ x: 8,
+ y: null,
+ },
+ },
+ {
+ x: 9,
+ y1: null,
+ y0: null,
+ initialY1: null,
+ initialY0: null,
+ datum: {
+ x: 9,
+ y: null,
+ },
+ },
+ {
+ x: 10,
+ y1: null,
+ y0: null,
+ initialY1: null,
+ initialY0: null,
+ datum: {
+ x: 10,
+ y: null,
+ },
+ },
+ {
+ x: 11,
+ y1: 12,
+ y0: 0,
+ initialY1: 12,
+ initialY0: null,
+ datum: {
+ x: 11,
+ y: 12,
+ },
+ },
+ {
+ x: 12,
+ y1: null,
+ y0: null,
+ initialY1: null,
+ initialY0: null,
+ datum: {
+ x: 12,
+ y: null,
+ },
+ },
+];
diff --git a/src/mocks/series/index.ts b/src/mocks/series/index.ts
new file mode 100644
index 0000000000..48a723d4ab
--- /dev/null
+++ b/src/mocks/series/index.ts
@@ -0,0 +1,2 @@
+export * from './series';
+export * from './utils';
diff --git a/src/mocks/series/series.ts b/src/mocks/series/series.ts
new file mode 100644
index 0000000000..d8ba605dc8
--- /dev/null
+++ b/src/mocks/series/series.ts
@@ -0,0 +1,181 @@
+import { shuffle } from 'lodash';
+
+import { mergePartial } from '../../utils/commons';
+import { getSpecId } from '../..';
+import {
+ DataSeries,
+ DataSeriesDatum,
+ RawDataSeries,
+ RawDataSeriesDatum,
+} from '../../chart_types/xy_chart/utils/series';
+import { fitFunctionData } from './data';
+import { FullDataSeriesDatum, WithIndex } from '../../chart_types/xy_chart/utils/fit_function';
+
+export class MockDataSeries {
+ private static readonly base: DataSeries = {
+ specId: getSpecId('spec1'),
+ key: ['spec1'],
+ seriesColorKey: 'spec1',
+ data: [],
+ };
+
+ static default(partial?: Partial) {
+ return mergePartial(MockDataSeries.base, partial, { mergeOptionalPartialValues: true });
+ }
+
+ static fitFunction(
+ options: { shuffle?: boolean; ordinal?: boolean } = { shuffle: true, ordinal: false },
+ ): DataSeries {
+ const ordinalData = options.ordinal
+ ? fitFunctionData.map((d) => ({ ...d, x: String.fromCharCode(97 + (d.x as number)) }))
+ : fitFunctionData;
+ const data = options.shuffle && !options.ordinal ? shuffle(ordinalData) : ordinalData;
+
+ return {
+ ...MockDataSeries.base,
+ data,
+ };
+ }
+
+ static withData(data: DataSeries['data']): DataSeries {
+ return {
+ ...MockDataSeries.base,
+ data,
+ };
+ }
+}
+
+export class MockRawDataSeries {
+ private static readonly base: RawDataSeries = {
+ specId: getSpecId('spec1'),
+ key: ['spec1'],
+ seriesColorKey: 'spec1',
+ data: [],
+ };
+
+ static default(partial?: Partial) {
+ return mergePartial(MockRawDataSeries.base, partial);
+ }
+
+ static fitFunction(
+ options: { shuffle?: boolean; ordinal?: boolean } = { shuffle: true, ordinal: false },
+ ): RawDataSeries {
+ const rawData = fitFunctionData.map(({ initialY0, initialY1, filled, ...datum }) => datum);
+ const ordinalData = options.ordinal
+ ? rawData.map((d) => ({ ...d, x: String.fromCharCode(97 + (d.x as number)) }))
+ : rawData;
+ const data = options.shuffle && !options.ordinal ? shuffle(ordinalData) : ordinalData;
+
+ return {
+ ...MockRawDataSeries.base,
+ data,
+ };
+ }
+
+ static withData(data: RawDataSeries['data']): RawDataSeries {
+ return {
+ ...MockRawDataSeries.base,
+ data,
+ };
+ }
+}
+
+export class MockDataSeriesDatum {
+ private static readonly base: DataSeriesDatum = {
+ x: 1,
+ y1: 1,
+ y0: 1,
+ initialY1: 1,
+ initialY0: 1,
+ datum: {},
+ };
+
+ static default(partial?: Partial): DataSeriesDatum {
+ return mergePartial(MockDataSeriesDatum.base, partial, { mergeOptionalPartialValues: true });
+ }
+
+ /**
+ * Fill datum with minimal values, default missing required values to `null`
+ */
+ static simple({
+ x,
+ y1 = null,
+ y0 = null,
+ filled,
+ }: Partial & Pick): DataSeriesDatum {
+ return {
+ x,
+ y1,
+ y0,
+ initialY1: y1,
+ initialY0: y0,
+ ...(filled && filled),
+ };
+ }
+
+ /**
+ * returns "full" datum with minimal values, default missing required values to `null`
+ *
+ * "full" - means x and y1 values are `non-nullable`
+ */
+ static full({
+ fittingIndex = 0,
+ ...datum
+ }: Partial> & Pick, 'x' | 'y1'>): WithIndex<
+ FullDataSeriesDatum
+ > {
+ return {
+ ...(MockDataSeriesDatum.simple(datum) as WithIndex),
+ fittingIndex,
+ };
+ }
+
+ static ordinal(partial?: Partial): DataSeriesDatum {
+ return mergePartial(
+ {
+ ...MockDataSeriesDatum.base,
+ x: 'a',
+ },
+ partial,
+ { mergeOptionalPartialValues: true },
+ );
+ }
+}
+
+export class MockRawDataSeriesDatum {
+ private static readonly base: RawDataSeriesDatum = {
+ x: 1,
+ y1: 1,
+ y0: 1,
+ datum: {},
+ };
+
+ static default(partial?: Partial): RawDataSeriesDatum {
+ return mergePartial(MockRawDataSeriesDatum.base, partial);
+ }
+
+ /**
+ * Fill raw datum with minimal values, default missing required values to `null`
+ */
+ static simple({
+ x,
+ y1 = null,
+ y0 = null,
+ }: Partial & Pick): RawDataSeriesDatum {
+ return {
+ x,
+ y1,
+ y0,
+ };
+ }
+
+ static ordinal(partial?: Partial): RawDataSeriesDatum {
+ return mergePartial(
+ {
+ ...MockRawDataSeriesDatum.base,
+ x: 'a',
+ },
+ partial,
+ );
+ }
+}
diff --git a/src/mocks/series/utils.ts b/src/mocks/series/utils.ts
new file mode 100644
index 0000000000..ef99a18f59
--- /dev/null
+++ b/src/mocks/series/utils.ts
@@ -0,0 +1,30 @@
+import { DataSeriesDatum } from '../../chart_types/xy_chart/utils/series';
+import { getYValue } from '../../chart_types/xy_chart/rendering/rendering';
+
+/**
+ * Helper function to return array of rendered y1 values
+ */
+export const getFilledNullData = (data: DataSeriesDatum[]): (number | undefined)[] => {
+ return data.filter(({ y1 }) => y1 === null).map(({ filled }) => filled && filled.y1);
+};
+
+/**
+ * Helper function to return array of rendered y1 values
+ */
+export const getFilledNonNullData = (data: DataSeriesDatum[]): (number | undefined)[] => {
+ return data.filter(({ y1 }) => y1 !== null).map(({ filled }) => filled && filled.y1);
+};
+
+/**
+ * Helper function to return array of rendered x values
+ */
+export const getXValueData = (data: DataSeriesDatum[]): (number | string)[] => {
+ return data.map(({ x }) => x);
+};
+
+/**
+ * Returns value of `y1` or `filled.y1` or null
+ */
+export const getYResolvedData = (data: DataSeriesDatum[]): (number | null)[] => {
+ return data.map(getYValue);
+};
diff --git a/src/mocks/specs/index.ts b/src/mocks/specs/index.ts
new file mode 100644
index 0000000000..60a182ae95
--- /dev/null
+++ b/src/mocks/specs/index.ts
@@ -0,0 +1 @@
+export * from './specs';
diff --git a/src/mocks/specs/specs.ts b/src/mocks/specs/specs.ts
new file mode 100644
index 0000000000..c889979131
--- /dev/null
+++ b/src/mocks/specs/specs.ts
@@ -0,0 +1,101 @@
+import { mergePartial } from '../../utils/commons';
+import {
+ SeriesSpecs,
+ DEFAULT_GLOBAL_ID,
+ BarSeriesSpec,
+ AreaSeriesSpec,
+ HistogramModeAlignments,
+ HistogramBarSeriesSpec,
+ LineSeriesSpec,
+ BasicSeriesSpec,
+} from '../../chart_types/xy_chart/utils/specs';
+import { getSpecId, getGroupId, SpecId } from '../../utils/ids';
+import { ScaleType } from '../../utils/scales/scales';
+
+export class MockSeriesSpec {
+ private static readonly barBase: BarSeriesSpec = {
+ id: getSpecId('spec1'),
+ seriesType: 'bar',
+ groupId: getGroupId(DEFAULT_GLOBAL_ID),
+ xScaleType: ScaleType.Ordinal,
+ yScaleType: ScaleType.Linear,
+ xAccessor: 'x',
+ yAccessors: ['y'],
+ yScaleToDataExtent: false,
+ hideInLegend: false,
+ enableHistogramMode: false,
+ stackAsPercentage: false,
+ data: [],
+ };
+
+ private static readonly histogramBarBase: HistogramBarSeriesSpec = {
+ id: getSpecId('spec1'),
+ seriesType: 'bar',
+ groupId: getGroupId(DEFAULT_GLOBAL_ID),
+ xScaleType: ScaleType.Ordinal,
+ yScaleType: ScaleType.Linear,
+ xAccessor: 'x',
+ yAccessors: ['y'],
+ yScaleToDataExtent: false,
+ hideInLegend: false,
+ enableHistogramMode: true,
+ data: [],
+ };
+
+ private static readonly areaBase: AreaSeriesSpec = {
+ id: getSpecId('spec1'),
+ seriesType: 'area',
+ groupId: getGroupId(DEFAULT_GLOBAL_ID),
+ xScaleType: ScaleType.Ordinal,
+ yScaleType: ScaleType.Linear,
+ xAccessor: 'x',
+ yAccessors: ['y'],
+ yScaleToDataExtent: false,
+ hideInLegend: false,
+ histogramModeAlignment: HistogramModeAlignments.Center,
+ data: [],
+ };
+
+ private static readonly lineBase: LineSeriesSpec = {
+ id: getSpecId('spec1'),
+ seriesType: 'line',
+ groupId: getGroupId(DEFAULT_GLOBAL_ID),
+ xScaleType: ScaleType.Ordinal,
+ yScaleType: ScaleType.Linear,
+ xAccessor: 'x',
+ yAccessors: ['y'],
+ yScaleToDataExtent: false,
+ hideInLegend: false,
+ histogramModeAlignment: HistogramModeAlignments.Center,
+ data: [],
+ };
+
+ static bar(partial?: Partial): BarSeriesSpec {
+ return mergePartial(MockSeriesSpec.barBase, partial, { mergeOptionalPartialValues: true });
+ }
+
+ static histogramBar(partial?: Partial): HistogramBarSeriesSpec {
+ return mergePartial(MockSeriesSpec.histogramBarBase, partial, {
+ mergeOptionalPartialValues: true,
+ });
+ }
+
+ static area(partial?: Partial): AreaSeriesSpec {
+ return mergePartial(MockSeriesSpec.areaBase, partial, { mergeOptionalPartialValues: true });
+ }
+
+ static line(partial?: Partial): LineSeriesSpec {
+ return mergePartial(MockSeriesSpec.lineBase, partial, { mergeOptionalPartialValues: true });
+ }
+}
+
+export class MockSeriesSpecs {
+ static fromSpecs(specs: BasicSeriesSpec[]): SeriesSpecs {
+ const specsMap: [SpecId, BasicSeriesSpec][] = specs.map((spec) => [spec.id, spec]);
+ return new Map(specsMap);
+ }
+
+ static empty(): SeriesSpecs {
+ return new Map();
+ }
+}
diff --git a/src/mocks/utils.ts b/src/mocks/utils.ts
new file mode 100644
index 0000000000..a7bd61e59a
--- /dev/null
+++ b/src/mocks/utils.ts
@@ -0,0 +1,10 @@
+/**
+ * Forces object to be partial type for mocking tests
+ *
+ * SHOULD NOT BE USED OUTSIDE OF TESTS!!!
+ *
+ * @param obj partial object type
+ */
+export const forcedType = (obj: Partial): T => {
+ return obj as T;
+};
diff --git a/src/utils/commons.ts b/src/utils/commons.ts
index 84201a1296..f928d8d72b 100644
--- a/src/utils/commons.ts
+++ b/src/utils/commons.ts
@@ -12,9 +12,6 @@ export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
-// Can remove once we upgrade to TypesScript >= 3.5
-export type Omit = Pick>;
-
/**
* This function returns a function to generate ids.
* This can be used to generate unique, but predictable ids to pair labels
diff --git a/src/utils/curves.ts b/src/utils/curves.ts
index 426fe703c5..662f470ced 100644
--- a/src/utils/curves.ts
+++ b/src/utils/curves.ts
@@ -10,6 +10,7 @@ import {
curveStepAfter,
curveStepBefore,
} from 'd3-shape';
+import { $Values } from 'utility-types';
export const CurveType = Object.freeze({
CURVE_CARDINAL: 0 as 0,
@@ -24,17 +25,7 @@ export const CurveType = Object.freeze({
LINEAR: 9 as 9,
});
-export type CurveType =
- | typeof CurveType.CURVE_CARDINAL
- | typeof CurveType.CURVE_NATURAL
- | typeof CurveType.CURVE_MONOTONE_X
- | typeof CurveType.CURVE_MONOTONE_Y
- | typeof CurveType.CURVE_BASIS
- | typeof CurveType.CURVE_CATMULL_ROM
- | typeof CurveType.CURVE_STEP
- | typeof CurveType.CURVE_STEP_AFTER
- | typeof CurveType.CURVE_STEP_BEFORE
- | typeof CurveType.LINEAR;
+export type CurveType = $Values;
export function getCurveFactory(curveType: CurveType = CurveType.LINEAR) {
switch (curveType) {
diff --git a/src/utils/scales/scales.ts b/src/utils/scales/scales.ts
index 0a7ccd3119..587906b10e 100644
--- a/src/utils/scales/scales.ts
+++ b/src/utils/scales/scales.ts
@@ -1,3 +1,5 @@
+import { $Values } from 'utility-types';
+
export interface Scale {
domain: any[];
range: number[];
@@ -39,12 +41,7 @@ export const ScaleType = Object.freeze({
Time: 'time' as 'time',
});
-export type ScaleType =
- | typeof ScaleType.Linear
- | typeof ScaleType.Sqrt
- | typeof ScaleType.Log
- | typeof ScaleType.Time
- | typeof ScaleType.Ordinal;
+export type ScaleType = $Values;
export interface ScaleConfig {
accessor: (value: any) => any;
diff --git a/stories/mixed.tsx b/stories/mixed.tsx
index a152c9ba13..47619d757b 100644
--- a/stories/mixed.tsx
+++ b/stories/mixed.tsx
@@ -1,6 +1,8 @@
import { storiesOf } from '@storybook/react';
+import { select, number } from '@storybook/addon-knobs';
import { DateTime } from 'luxon';
import React from 'react';
+
import {
AreaSeries,
Axis,
@@ -15,6 +17,7 @@ import {
Settings,
} from '../src/';
import { timeFormatter } from '../src/utils/data/formatters';
+import { Fit } from '../src/chart_types/xy_chart/utils/specs';
storiesOf('Mixed Charts', module)
.add('bar and lines', () => {
@@ -217,4 +220,182 @@ storiesOf('Mixed Charts', module)
/>
);
+ })
+ .add('Fitting functions - non-stacked series', () => {
+ const dataTypes = {
+ isolated: [
+ { x: 0, y: 3 },
+ { x: 1, y: 5 },
+ { x: 2, y: null },
+ { x: 3, y: 4 },
+ { x: 4, y: null },
+ { x: 5, y: 5 },
+ { x: 6, y: null },
+ { x: 7, y: 12 },
+ { x: 8, y: null },
+ { x: 9, y: 10 },
+ { x: 10, y: 7 },
+ ],
+ successive: [
+ { x: 0, y: 3 },
+ { x: 1, y: 5 },
+ { x: 2, y: null },
+ { x: 4, y: null },
+ { x: 6, y: null },
+ { x: 8, y: null },
+ { x: 9, y: 10 },
+ { x: 10, y: 7 },
+ ],
+ endPoints: [
+ { x: 0, y: null },
+ { x: 1, y: 5 },
+ { x: 3, y: 4 },
+ { x: 5, y: 5 },
+ { x: 7, y: 12 },
+ { x: 9, y: 10 },
+ { x: 10, y: null },
+ ],
+ ordinal: [
+ { x: 'a', y: null },
+ { x: 'b', y: 3 },
+ { x: 'c', y: 5 },
+ { x: 'd', y: null },
+ { x: 'e', y: 4 },
+ { x: 'f', y: null },
+ { x: 'g', y: 5 },
+ { x: 'h', y: 6 },
+ { x: 'i', y: null },
+ { x: 'j', y: null },
+ { x: 'k', y: null },
+ { x: 'l', y: 12 },
+ { x: 'm', y: null },
+ ],
+ all: [
+ { x: 0, y: null },
+ { x: 1, y: 3 },
+ { x: 2, y: 5 },
+ { x: 3, y: null },
+ { x: 4, y: 4 },
+ { x: 5, y: null },
+ { x: 6, y: 5 },
+ { x: 7, y: 6 },
+ { x: 8, y: null },
+ { x: 9, y: null },
+ { x: 10, y: null },
+ { x: 11, y: 12 },
+ { x: 12, y: null },
+ ],
+ };
+
+ const seriesType = select(
+ 'seriesType',
+ {
+ Area: 'area',
+ Line: 'line',
+ },
+ 'area',
+ );
+ const dataKey = select(
+ 'dataset',
+ {
+ 'Isolated Points': 'isolated',
+ 'Successive null Points': 'successive',
+ 'null end points': 'endPoints',
+ 'Ordinal x values': 'ordinal',
+ 'All edge cases': 'all',
+ },
+ 'all',
+ );
+ // @ts-ignore
+ const dataset = dataTypes[dataKey];
+ const fit = select(
+ 'fitting function',
+ {
+ None: Fit.None,
+ Carry: Fit.Carry,
+ Lookahead: Fit.Lookahead,
+ Nearest: Fit.Nearest,
+ Average: Fit.Average,
+ Linear: Fit.Linear,
+ Zero: Fit.Zero,
+ Explicit: Fit.Explicit,
+ },
+ Fit.Average,
+ );
+ const curve = select(
+ 'Curve',
+ {
+ 'Curve cardinal': CurveType.CURVE_CARDINAL,
+ 'Curve natural': CurveType.CURVE_NATURAL,
+ 'Curve monotone x': CurveType.CURVE_MONOTONE_X,
+ 'Curve monotone y': CurveType.CURVE_MONOTONE_Y,
+ 'Curve basis': CurveType.CURVE_BASIS,
+ 'Curve catmull rom': CurveType.CURVE_CATMULL_ROM,
+ 'Curve step': CurveType.CURVE_STEP,
+ 'Curve step after': CurveType.CURVE_STEP_AFTER,
+ 'Curve step before': CurveType.CURVE_STEP_BEFORE,
+ Linear: CurveType.LINEAR,
+ },
+ 0,
+ );
+ const endValue = select(
+ 'End value',
+ {
+ None: 'none',
+ nearest: 'nearest',
+ '0': 0,
+ '2': 2,
+ },
+ 'none',
+ );
+ const parsedEndValue: number | 'nearest' = Number.isNaN(Number(endValue)) ? 'nearest' : Number(endValue);
+ const value = number('Explicit valuve (using Fit.Explicit)', 5);
+ const xScaleType = dataKey === 'ordinal' ? ScaleType.Ordinal : ScaleType.Linear;
+
+ return (
+
+
+
+
+ {seriesType === 'area' ? (
+
+ ) : (
+
+ )}
+
+ );
});
diff --git a/tsconfig.jest.json b/tsconfig.jest.json
index 5cd2e875f6..f407711d1d 100644
--- a/tsconfig.jest.json
+++ b/tsconfig.jest.json
@@ -1,6 +1,8 @@
{
- "extends": "./tsconfig",
+ "extends": "./tsconfig.json",
"include": [
- "src/**/*"
+ "src/**/*",
+ "scripts/setup_enzyme.ts",
+ "scripts/custom_matchers.ts"
]
}
diff --git a/tsconfig.json b/tsconfig.json
index 9e69b0d363..78daac7661 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -15,6 +15,7 @@
"jsx": "react",
"allowJs": false,
"skipLibCheck": true,
- "downlevelIteration": true
+ "downlevelIteration": true,
+ "typeRoots": ["./node_modules/@types/", "./global.d.ts"]
}
}
diff --git a/wiki/consuming.md b/wiki/consuming.md
index 013ec2da6e..83813ccc03 100644
--- a/wiki/consuming.md
+++ b/wiki/consuming.md
@@ -15,6 +15,7 @@ To use Elastic Charts code in Kibana, check if `@elastic/charts` packages is alr
## Using Elastic Charts in a standalone project
You can consume Elastic Charts in standalone projects, such as plugins and prototypes. Elastic-Charts has a peer dependency on [moment-timezone](https://momentjs.com/timezone/). Add that dependency on your main project with:
+
```
yarn add moment-timezone
```
@@ -63,6 +64,6 @@ Use a `.babelrc` config with the [`usebuiltins`](https://babeljs.io/docs/en/babe
Directly import polyfill and runtime.
```js
-import "core-js/stable";
-import "regenerator-runtime/runtime";
+import 'core-js/stable';
+import 'regenerator-runtime/runtime';
```
diff --git a/yarn.lock b/yarn.lock
index bfa3dd42e5..596bce006d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6780,7 +6780,7 @@ expect-puppeteer@^4.3.0:
resolved "https://registry.yarnpkg.com/expect-puppeteer/-/expect-puppeteer-4.3.0.tgz#732a3c94ab44af0c7d947040ad3e3637a0359bf3"
integrity sha512-p8N/KSVPG9PAOJlftK5f1n3JrULJ6Qq1EQ8r/n9xzkX2NmXbK8PcnJnkSAEzEHrMycELKGnlJV7M5nkgm+wEWA==
-expect@^24.9.0:
+expect@^24.1.0, expect@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca"
integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==
@@ -9122,6 +9122,20 @@ jest-environment-puppeteer@^4.3.0:
jest-dev-server "^4.3.0"
merge-deep "^3.0.2"
+jest-extended@^0.11.2:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/jest-extended/-/jest-extended-0.11.2.tgz#924f4a6b4c946133faf9ec8fba865de9790f4116"
+ integrity sha512-gwNMXrAPN0IY5L7VXWfSlC2aGo0KHIsGGcW+lTHYpedt5SJksEvBgMxs29iNikiNOz+cqAZY1s/+kYK0jlj4Jw==
+ dependencies:
+ expect "^24.1.0"
+ jest-get-type "^22.4.3"
+ jest-matcher-utils "^22.0.0"
+
+jest-get-type@^22.4.3:
+ version "22.4.3"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4"
+ integrity sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==
+
jest-get-type@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e"
@@ -9190,6 +9204,15 @@ jest-leak-detector@^24.9.0:
jest-get-type "^24.9.0"
pretty-format "^24.9.0"
+jest-matcher-utils@^22.0.0:
+ version "22.4.3"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz#4632fe428ebc73ebc194d3c7b65d37b161f710ff"
+ integrity sha512-lsEHVaTnKzdAPR5t4B6OcxXo9Vy4K+kRRbG5gtddY8lBEC+Mlpvm1CJcsMESRjzUhzkz568exMV1hTB76nAKbA==
+ dependencies:
+ chalk "^2.0.1"
+ jest-get-type "^22.4.3"
+ pretty-format "^22.4.3"
+
jest-matcher-utils@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073"
@@ -10135,7 +10158,7 @@ lodash@4.17.14:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
-lodash@4.17.15, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.4, lodash@^4.2.1, lodash@~4.17.10:
+lodash@4.17.15, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1, lodash@~4.17.10:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -12389,6 +12412,14 @@ pretty-error@^2.1.1:
renderkid "^2.0.1"
utila "~0.4"
+pretty-format@^22.4.3:
+ version "22.4.3"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-22.4.3.tgz#f873d780839a9c02e9664c8a082e9ee79eaac16f"
+ integrity sha512-S4oT9/sT6MN7/3COoOy+ZJeA92VmOnveLHgrwBE3Z1W5N9S2A1QGNYiE1z75DAENbJrXXUb+OWXhpJcg05QKQQ==
+ dependencies:
+ ansi-regex "^3.0.0"
+ ansi-styles "^3.2.0"
+
pretty-format@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9"
@@ -15855,6 +15886,11 @@ utila@^0.4.0, utila@~0.4:
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
+utility-types@^3.8.0:
+ version "3.8.0"
+ resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.8.0.tgz#eaae1c2520c206b5623a4f07af38e3aa31dc2f06"
+ integrity sha512-UoKivAmVw5TL6AHuILieOJjIK9ajS0l5gyN5LbJglPuVwzfYBniDhe+3A5+ZBtS0TAHQs5qUxTwj9jXurINrcw==
+
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"