Merge pull request #21113 from apache/fix/visualMap-range

VisualMap support unboundedRange
diff --git a/src/component/visualMap/ContinuousModel.ts b/src/component/visualMap/ContinuousModel.ts
index 5698426..35e14aa 100644
--- a/src/component/visualMap/ContinuousModel.ts
+++ b/src/component/visualMap/ContinuousModel.ts
@@ -45,12 +45,30 @@
     calculable?: boolean
 
     /**
-     * selected range. In default case `range` is [min, max]
-     * and can auto change along with modification of min max,
+     * selected range. In default case `range` is `[min, max]`
+     * and can auto change along with user interaction or action "selectDataRange",
      * until user specified a range.
+     * @see unboundedRange for the special case when `range[0]` or `range[1]` touch `min` or `max`.
      */
     range?: number[]
     /**
+     * Whether to treat the range as unbounded when `range` touches `min` or `max`.
+     * - `true`:
+     *   when `range[0]` <= `min`, the actual range becomes `[-Infinity, range[1]]`;
+     *   when `range[1]` >= `max`, the actual range becomes `[range[0], Infinity]`.
+     *   NOTE:
+     *     - This provides a way to ensure all data can be considered in-range when `min`/`max`
+     *       are not precisely known.
+     *     - Default is `true` for backward compatibility.
+     *     - Piecewise VisualMap does not need it, since it can define unbounded range in each piece,
+     *       such as "< 12", ">= 300".
+     * - `false`:
+     *   Disable the unbounded range behavior.
+     *   Use case: `min`/`max` reflect the normal data range, and some outlier data should always be
+     *   treated as out of range.
+     */
+    unboundedRange?: boolean
+    /**
      * Whether to enable hover highlight.
      */
     hoverLink?: boolean
@@ -183,8 +201,13 @@
      */
     getValueState(value: number): VisualState {
         const range = this.option.range;
+        const dataExtent = this.getExtent();
+        const unboundedRange = zrUtil.retrieve2(this.option.unboundedRange, true);
 
-        return range[0] <= value && value <= range[1] ? 'inRange' : 'outOfRange';
+        return (
+            ((unboundedRange && range[0] <= dataExtent[0]) || range[0] <= value)
+            && ((unboundedRange && range[1] >= dataExtent[1]) || value <= range[1])
+        ) ? 'inRange' : 'outOfRange';
     }
 
     findTargetDataIndices(range: number[]) {
@@ -198,7 +221,7 @@
             const dataIndices: number[] = [];
             const data = seriesModel.getData();
 
-            data.each(this.getDataDimensionIndex(data), function (value, dataIndex) {
+            data.each(this.getDataDimensionIndex(data), function (value: number, dataIndex) {
                 range[0] <= value && value <= range[1] && dataIndices.push(dataIndex);
             }, this);
 
diff --git a/test/radar2.html b/test/radar2.html
index 47b7f59..87cdba5 100644
--- a/test/radar2.html
+++ b/test/radar2.html
@@ -61,7 +61,9 @@
                         })()
                     },
                     visualMap: {
-                        color: ['red', 'yellow']
+                        inRange: {
+                            color: ['red', 'yellow']
+                        },
                     },
                     radar: {
                        indicator : [
diff --git a/test/runTest/actions/__meta__.json b/test/runTest/actions/__meta__.json
index 757ec4b..39b1bf8 100644
--- a/test/runTest/actions/__meta__.json
+++ b/test/runTest/actions/__meta__.json
@@ -237,5 +237,6 @@
   "universalTransition3": 2,
   "visualMap-categories": 1,
   "visualMap-multi-continuous": 1,
+  "visualMap-on-data": 1,
   "visualMap-selectMode": 2
 }
\ No newline at end of file
diff --git a/test/visualMap-on-data.html b/test/visualMap-on-data.html
index 34966d9..c0dc309 100644
--- a/test/visualMap-on-data.html
+++ b/test/visualMap-on-data.html
@@ -37,6 +37,7 @@
 
 
         <div id="main0"></div>
+        <div id="main-unbounded-continuous"></div>
 
 
         <script>
@@ -82,5 +83,80 @@
             });
 
         </script>
+
+
+
+        <script>
+
+            var option;
+
+            require([
+                'echarts'
+            ], function (echarts) {
+
+                var option = {
+                    tooltip: {
+                    },
+                    visualMap: {
+                        type: 'continuous',
+                        top: 'middle',
+                        calculable: true,
+                        min: 0,
+                        max: 1000,
+                        inRange: {
+                            color: ['red', 'blue']
+                        },
+                        outOfRange: {
+                            color: ['#999']
+                        },
+                    },
+                    xAxis: {
+                        type: 'value',
+                    },
+                    yAxis: {
+                        type: 'value',
+                    },
+                    series: [{
+                        type: 'scatter',
+                        itemStyle: {
+                            color: 'green'
+                        },
+                        data: [
+                            [10, 712],
+                            [20, 920],
+                            [10, 503],
+                            [40, 1020],
+                            [50, -100],
+                            [60, 420],
+                            [70, -10]
+                        ]
+                    }]
+                };
+
+                var chart = testHelper.create(echarts, 'main-unbounded-continuous', {
+                    option: option,
+                    title: [
+                        '**unboundedRange** in continuous visualMap.',
+                        'outliers (>= 1000 or <= 0) should be always be treated as out of range.',
+                    ],
+                    inputs: [
+                        {
+                            type: 'select',
+                            text: 'unboundedRange:',
+                            values: [true, false],
+                            onChange: function (value) {
+                                chart.setOption({
+                                    visualMap: {
+                                        unboundedRange: this.value
+                                    }
+                                });
+                            }
+                        }
+                    ]
+                });
+            });
+
+        </script>
+
     </body>
 </html>
\ No newline at end of file