时间

字数统计: 3.4k
阅读时长: 约 11 分钟
当前版本: 4.29

使用简单渲染器、视觉变量和聚类来样式化事件,以显示要关闭的天数

什么是时间样式?

时间样式解决日期/时间数据相关的可视化,表达事件发生的时间和地点。常见示例包括:

  1. 时间线
  2. 之前和之后
  3. 期限
  4. 几何动画
  5. 属性动画

示例

时间线

时间线可视化效果使用连续色带显示日期/时间数据。它提供了一个即时视图,表示现象发生的时间或相对于所有其他数据点记录值的时间。

以下示例显示了 2000 年大西洋中的飓风位置。它使用颜色视觉变量来显示哪些飓风发生在季节早期,哪些飓风发生在季节后期。

示例代码
js
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />

    <title> GeoScene Developer Guide: Hurricane location and timeline </title>

    <link rel="stylesheet" href="https://js.geoscene.cn/4.29/geoscene/themes/light/main.css" />
    <script src="https://js.geoscene.cn/4.29/"></script>

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }
    </style>

    <script>
      require([
        "geoscene/Map",
        "geoscene/views/MapView",
        "geoscene/layers/FeatureLayer",
        "geoscene/widgets/Legend",
        "geoscene/widgets/Expand"
      ], function (Map, MapView, FeatureLayer, Legend, Expand) {
        const renderer = {
          type: "simple",
          label: "Observed hurricane location",
          symbol: {
            type: "simple-marker",
            size: 6,
            outline: {
              width: 0.5,
              color: [255,255,255,0.5]
            }
          },
          visualVariables: [{
            type: "color",
            field: "Date_Time",
            legendOptions: {
              title: "Date of observation"
            },
            stops: [
              { value: new Date(2000, 7, 3).getTime(), color: "#00ffff"},
              { value: new Date(2000, 9, 22).getTime(), color: "#2f3f56"}
            ]
          }]
        };

        const layer = new FeatureLayer({
          url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Hurricanes/MapServer/0",
          renderer: renderer
        });

        const map = new Map({
          basemap: "tianditu-vector",
          layers: [ layer ]
        });

        const view = new MapView({
          container: "viewDiv",
          map: map,
          scale: 36978595,
          center: [ -44.0632, 33.1567 ],
          constraints: {
            snapToZoom:false
          }
        });

        view.ui.add(new Expand({
          content: new Legend({
            view: view
          }),
          view: view,
          expanded: true
        }), "top-right");

      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
  </body>
</html>

之前和之后

您可以使用具有发散色带的颜色视觉变量来可视化特定日期之前和之后发生的事件。

以下示例可视化了飓风位置的聚类,这些聚类按它们平均发生在 1977 年 1 月 1 日之前还是之后进行汇总。

示例代码
js
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />

    <title>GeoScene Developer Guide: Hurricane locations before or after 1977</title>

    <link rel="stylesheet" href="https://js.geoscene.cn/4.29/geoscene/themes/light/main.css" />
    <script src="https://js.geoscene.cn/4.29/"></script>

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }
    </style>

    <script>
      require([
        "geoscene/Map",
        "geoscene/views/MapView",
        "geoscene/layers/FeatureLayer",
        "geoscene/widgets/Legend",
        "geoscene/widgets/Expand"
      ], function (Map, MapView, FeatureLayer, Legend, Expand) {
        const colors = [ "#54bebe", "#dedad2", "#c80064" ];
        const renderer = {
          type: "simple",
          label: "Observed hurricane location",
          symbol: {
            type: "simple-marker",
            size: 4,
            outline: null
          },
          // @@Start(visualVariables)
          visualVariables: [{
            type: "color",
            field: "ISO_time",
            legendOptions: {
              title: "Before and after July 1"
            },
            stops: [
              { value: new Date(2013, 0, 1).getTime(), color: colors[0], label: "January 1" },
              { value: new Date(2013, 6, 1).getTime(), color: colors[1], label: "July 1" },
              { value: new Date(2013, 11, 31).getTime(), color: colors[2], label: "December 31" }
            ]
          }]
        };

        // ISO_time

        const layer = new FeatureLayer({
          title: "2013 Hurricane season",
          url:"https://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/Hurricanes_and_storms/FeatureServer",
          renderer: renderer,
          definitionExpression: "Season = 2013"
        });

        const map = new Map({
          basemap: "tianditu-vector",
          layers: [ layer ]
        });

        const view = new MapView({
          container: "viewDiv",
          map: map,
          scale: 136195673,
          constraints: {
            snapToZoom: false,
            rotationEnabled: false
          }
        });

        view.ui.add(new Expand({
          content: new Legend({
            view: view
          }),
          view: view,
          expanded: true
        }), "top-right");

      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
  </body>
</html>

期限

期限可视化根据日期字段显示要素的期限(或到有意义日期或当前日期的已用时间)。期限可视化可应用于以下情景:

  1. 资产投入使用多久了?

  2. 事件报告有多久了?

  3. 自上次检查以来多久了?

  4. 事件是否已超出了解决的期限?

期限通常使用 Arcade 表达式中的 DateDiff 函数进行定义,并使用颜色或大小变量进行映射。它使用所需的单位指定时间长度。

js
valueExpression = "DateDiff( Now(), $feature.date_created, 'hours' )"

以下示例根据事件结束前相对于截止日期所经过的时间,来可视化已聚类的街道投诉状况。

示例代码
js
<!DOCTYPE html>
<html>
    <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />

    <title> GeoScene Developer Guide: Street condition complaints</title>

    <link rel="stylesheet" href="https://js.geoscene.cn/4.29/geoscene/themes/light/main.css" />
    <script src="https://js.geoscene.cn/4.29/"></script>

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }
    </style>

    <script>
      require([
        "geoscene/Map",
        "geoscene/views/MapView",
        "geoscene/layers/FeatureLayer",
        "geoscene/widgets/Legend",
        "geoscene/widgets/Expand",
        "geoscene/smartMapping/labels/clusters",
        "geoscene/smartMapping/popup/clusters",
        "geoscene/core/promiseUtils"
      ], function (Map, MapView, FeatureLayer, Legend, Expand,
        clusterLabelCreator,
        clusterPopupCreator,
        promiseUtils
      ) {
        const colors = [ "#2799ff", "#423a3a", "#ff3333" ];
        const renderer = {
          type: "simple",
          label: "Observed hurricane location",
          symbol: {
            type: "simple-marker",
            size: 6,
            outline: {
              width: 0.5,
              color: [255,255,255,0.5]
            }
          },
          visualVariables: [{
            type: "color",
            valueExpression: "DateDiff($feature['closed_time'], $feature['due_time'], 'days')",
            valueExpressionTitle: "Days it took to close incident",
            stops: [
              { value: -21, color: colors[0], label: "21 weeks early" },
              { value: 0, color: colors[1], label: "On time" },
              { value: 21, color: colors[2], label: "21 weeks late" }
            ]
          }]
        };

        const layer = new FeatureLayer({
          url: "https://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/nyc_311_2015_first_50k/FeatureServer",
          title: "Street condition complaints",
          renderer: renderer,
          definitionExpression: "Complaint_Type = 'Street Condition'"
        });

        const map = new Map({
          basemap: "tianditu-vector",
          layers: [ layer ]
        });

        const view = new MapView({
          container: "viewDiv",
          map: map,
          scale: 1244447,
          center: [ -73.863, 40.7 ],
          constraints: {
            snapToZoom:false
          }
        });

        view.ui.add(new Expand({
          content: new Legend({
            view: view
          }),
          view: view,
          expanded: true
        }), "top-right");

        view.when()
          .then(generateClusterConfig)
          .then(function(featureReduction){
            layer.featureReduction = featureReduction;
          });

          function generateClusterConfig() {
          // generates default popupTemplate
          const popupPromise = clusterPopupCreator
            .getTemplates({
              layer: layer
            })
            .then(function (popupTemplateResponse) {
              return popupTemplateResponse.primaryTemplate.value;
            });

          // generates default labelingInfo
          const labelPromise = clusterLabelCreator
            .getLabelSchemes({
              layer: layer,
              view: view
            })
            .then(function (labelSchemes) {
              return labelSchemes.primaryScheme;
            });

          return promiseUtils
            .eachAlways([ popupPromise, labelPromise ])
            .then(function (result) {
              const popupTemplate = result[0].value;

              const primaryLabelScheme = result[1].value;
              const labelingInfo = primaryLabelScheme.labelingInfo;
              // Ensures the clusters are large enough to fit labels
              const clusterMinSize = primaryLabelScheme.clusterMinSize;

              return {
                type: "cluster",
                popupTemplate: popupTemplate,
                labelingInfo: labelingInfo,
                clusterMinSize: clusterMinSize,
                clusterRadius: 50
              };
            })
            .catch(function (error) {
              console.error(error);
            });
        }
      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
  </body>
</html>

几何动画

在大多数情况下,时序动画涉及为图层分配静态渲染器并随时间过滤要素。对于仅在特定时刻或时间窗口(例如地震、飓风、飞机旅行)出现的要素,这很有效。这需要 TimeSlider 微件来控制时间窗口。

此示例中的图层使用简单渲染器和显示旋转飓风符号的动画 GIF 进行渲染。此图层显示飓风相对于滑块拇指位置的当前位置。将添加另一个图层以显示飓风相对于时间滑块值的先前位置。

示例代码
js
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />

    <title>GeoScene Developer Guide: Time series filter</title>

    <link rel="stylesheet" href="https://js.geoscene.cn/4.29/geoscene/themes/light/main.css" />
    <script src="https://js.geoscene.cn/4.29/"></script>

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }

      #timeSlider {
        position: absolute;
        left: 100px;
        right: 100px;
        bottom: 30px;
      }
    </style>

    <script>
      require([
        "geoscene/Map",
        "geoscene/views/MapView",
        "geoscene/layers/FeatureLayer",
        "geoscene/widgets/Legend",
        "geoscene/widgets/TimeSlider",
        "geoscene/widgets/Expand"
      ], function (Map, MapView, FeatureLayer, Legend, TimeSlider, Expand) {
        const year = 2005;
        const renderer = {
          type: "simple",
          label: "Observed hurricane location",
          symbol: {
            type: "picture-marker",
            url: "./assets/img/guide/visualization/demos/cyclone-marker.gif",
            height: 20,
            width: 20
          },
          visualVariables: [{
            type: "size",
            field: "Category",
            stops: [
              { value: 1, size: 12 },
              { value: 2, size: 16 },
              { value: 3, size: 20 },
              { value: 4, size: 24 },
              { value: 5, size: 28 }
            ]
          }]
        };

        const layer = new FeatureLayer({
          title: `${year} Hurricane season`,
          url:"https://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/Hurricanes_and_storms/FeatureServer",
          renderer: renderer,
          definitionExpression: `Season = ${year}`
        });

        const trackRenderer = {
          type: "simple",
          label: "Previous hurricane location",
          symbol: {
            type: "simple-marker",
            color: "rgba(188,46,28,0.05)",
            size: 4,
            outline: null
          },
          visualVariables: [{
            type: "size",
            field: "Category",
            legendOptions: {
              showLegend: false
            },
            stops: [
              { value: 1, size: 4 },
              { value: 2, size: 8 },
              { value: 3, size: 10 },
              { value: 4, size: 12 },
              { value: 5, size: 18 }
            ]
          }]
        };

        const trackLayer = new FeatureLayer({
          title: `${year} Hurricane season`,
          url:"https://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/Hurricanes_and_storms/FeatureServer",
          renderer: trackRenderer,
          useViewTime: false,
          definitionExpression: `Season = ${year}`
        });

        const map = new Map({
          basemap: "tianditu-vector",
          layers: [ layer, trackLayer ]
        });

        const view = new MapView({
          container: "viewDiv",
          map: map,
          scale: 136195673,
          constraints: {
            snapToZoom: false,
            rotationEnabled: false
          }
        });

        view.ui.add(new Expand({
          content: new Legend({
            view: view
          }),
          view: view,
          expanded: false
        }), "top-right");

        const start = new Date(year-1, 9, 1);
        const end = new Date(year+1, 0, 15);
        const next = new Date(year-1, 9, 1, 7);
        const timeSlider = new TimeSlider({
          container: "timeSlider",
          playRate: 30,
          mode: "time-window",
          fullTimeExtent: {
            start,
            end
          },
          timeExtent: {
            start,
            end: next
          },
          stops: {
            interval: {
              value: 6,
              unit: "hours"
            }
          },
          view: view
        });
        view.ui.add(timeSlider, "manual");

        view.whenLayerView(trackLayer).then(function(trackLayerView){
          trackLayerView.filter = {
            timeExtent: timeSlider.timeExtent
          };

          timeSlider.watch("timeExtent", function(timeExtent){
            trackLayerView.filter = {
              timeExtent: {
                start,
                end: timeExtent.end
              }
            }
          });
        });

      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
    <div id="timeSlider"></div>
  </body>
</html>

属性动画

属性动画是动画可视化,用于显示固定在不变位置的要素中的属性如何随时间变化。这些通常涉及时间滑块,因此用户能够浏览时间时刻或时间窗口。

对于具有静态位置(例如传感器、建筑物、国家/地区等)但属性随时间变化(例如人口、温度、风速)的要素,您可以动态更新渲染器以引用与特定时刻相关的数据值,而不是过滤要素。这样就无需将重复几何下载到客户端。

渲染器会随着滑块值的每次更改而更新。

js
slider.on(["thumb-change", "thumb-drag"], (event) => {
    updateYearDisplay(event.value);
    updateRenderer(event.value);
    updateHistogram(event.value);
 });
示例代码
js
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="initial-scale=1,maximum-scale=1,user-scalable=no"
    />
    <title>GeoScene Developer Guide: Time series animation</title>

    <link rel="stylesheet" href="https://js.geoscene.cn/4.29/geoscene/themes/light/main.css" />
    <script src="https://js.geoscene.cn/4.29/"></script>

    <style>
      #containerDiv {
        padding: 10px;
        text-align: center;
        box-shadow: 0;
      }

      #sliderDiv {
        height: 100px;
      }

      #histogram {
        width: 500px;
        height: 150px;
      }
      .labels {
        padding: 5px;
      }

      html,
      body {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }

      #viewDiv {
        position: absolute;
        right: 0;
        left: 0;
        top: 0;
        bottom: 100px;
      }

      #sliderContainer {
        position: absolute;
        bottom: 0;
        height: 100px;
        width: 100%;
        text-align: center;
      }
    </style>

    <script id="highest-temp-arcade" type="geoscene/arcade">
      var highest = -Infinity;
      var ignoreFields = [ "OBJECTID", "x", "y", "Range" ];
      for (var att in $feature){
        var value = $feature[att];
        if( typeof(value) == 'Number' && IndexOf(ignoreFields, att) == -1){
          highest = IIF(value > highest, value, highest);
        }
      }
      return highest;
    </script>

    <script id="lowest-temp-arcade" type="geoscene/arcade">
      var lowest = Infinity;
      var ignoreFields = [ "OBJECTID", "x", "y", "Range" ];
      for (var att in $feature){
        var value = $feature[att];
        if( typeof(value) == 'Number' && IndexOf(ignoreFields, att) == -1){
          lowest = IIF(value < lowest, value, lowest);
        }
      }
      return lowest;
    </script>

    <script>
      require([
        "geoscene/Map",
        "geoscene/views/MapView",
        "geoscene/layers/FeatureLayer",
        "geoscene/smartMapping/statistics/histogram",
        "geoscene/smartMapping/statistics/summaryStatistics",
        "geoscene/widgets/Histogram",
        "geoscene/widgets/Slider",
        "geoscene/widgets/Legend",
        "geoscene/widgets/Expand",
        "geoscene/core/reactiveUtils",
        "geoscene/core/promiseUtils",
        "geoscene/Color"
      ], (
        Map,
        MapView,
        FeatureLayer,
        histogram,
        summaryStatistics,
        Histogram,
        Slider,
        Legend,
        Expand,
        reactiveUtils,
        promiseUtils,
        Color
      ) => {
        // Project base layer (world countries) to Equal Earth projection
        const baseLayer = new FeatureLayer({
          url:"https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/World_Countries_(Generalized)/FeatureServer",
          legendEnabled: false,
          popupEnabled: false,
          renderer: {
            type: "simple",
            symbol: {
              type: "simple-fill",
              color: [200, 200, 200, 0.75],
              outline: null
            }
          }
        });

        // Set initial temperature anomaly renderer on layer based
        // on data recorded for the year 1880

        const layer = new FeatureLayer({
          url: "https://services.arcgis.com/jIL9msH9OI208GCb/arcgis/rest/services/Global_Temperatures_1880_to_2018/FeatureServer/0",
          outFields: ["*"],
          title: "Temperatures by location (1880 - 2018)",
          renderer: {
            type: "simple",
            label: "Observation point",
            symbol: {
              type: "simple-marker",
              style: "diamond",
              size: "6px",
              color: [226, 226, 226, 0.75],
              outline: {
                color: [255, 255, 255, 0.25],
                width: "0.75px"
              }
            },
            visualVariables: [
              {
                type: "size",
                valueExpression: getSizeValueExpression(1880),
                valueExpressionTitle: "Absolute Value",
                legendOptions: {
                  showLegend: false
                },
                maxDataValue: 35,
                maxSize: "24px",
                minDataValue: 10,
                minSize: "4px"
              }, {
                type: "color",
                field: getColorField(1880),
                legendOptions: {
                  title: "Temperature Anomaly"
                },
                stops: [
                  {
                    value: -2.5,
                    color: [5, 112, 176, 0.75],
                    label: "Less than -2.5 deg C"
                  },
                  { value: -1, color: [208, 209, 230, 0.75] },
                  { value: -0.5, color: [236, 231, 242, 0.75] },
                  {
                    value: 0,
                    color: [226, 226, 226, 0.75],
                    label: "No difference/No Data"
                  },
                  { value: 0.5, color: [254, 232, 200, 0.75] },
                  { value: 1, color: [253, 212, 158, 0.75] },
                  {
                    value: 2.5,
                    color: [215, 48, 31, 0.75],
                    label: "More than 2.5 deg C"
                  }
                ]
              }
            ]
          },
          popupTemplate: {
            expressionInfos: [
              {
                name: "max",
                title: "Warmest anomaly",
                expression: document.getElementById("highest-temp-arcade")
                  .innerText
              },
              {
                name: "min",
                title: "Coldest anomaly",
                expression: document.getElementById("lowest-temp-arcade").innerText
              }
            ],
            content: [
              {
                type: "fields",
                fieldInfos: [
                  {
                    fieldName: "expression/max",
                    format: {
                      places: 2
                    }
                  },
                  {
                    fieldName: "expression/min",
                    format: {
                      places: 2
                    }
                  }
                ]
              }
            ]
          }
        });

        const map = new Map({
          basemap: "tianditu-vector",
          layers: [layer]
        });

        const spatialReference = {
          wkid: 54035
        };

        const view = new MapView({
          container: "viewDiv",
          map: map,
          scale: 150000000,
          center: { x: 0, y: 0, spatialReference: spatialReference },
          spatialReference: spatialReference,
          popup: {
            dockOptions: {
              position: "top-left"
            }
          },
          constraints: {
            snapToZoom:false
          }
        });

        view.ui.add(
          new Expand({
            view: view,
            content: document.getElementById("containerDiv"),
            expanded: false,
            expandIcon: "graph-bar"
          }),
          "top-right"
        );

        // This slider will allow the user to update the renderer based on a
        // provided year between 1880 and 2018

        const slider = new Slider({
          min: 1880,
          max: 2018,
          values: [ 1880 ],
          visibleElements: {
            labels: true,
            rangeLabels: true
          },
          labelInputsEnabled: true,
          precision: 0,
          steps: 1,
          container: "sliderDiv"
        });

        // When the user changes the slider's value,
        // change the renderer and histogram to reflect
        // data corresponding to the year indicated on the slider
        slider.on(["thumb-change", "thumb-drag"], (event) => {
          updateYearDisplay(event.value);
          updateRenderer(event.value);
          updateHistogram(event.value);
        });

        let lv = null;

        // Query all the features in the layer. These will by used
        // for client-side queries as the user slides the thumb of the slider

        view
          .whenLayerView(layer)
          .then(async (layerView) => {
            lv = layerView;
            await reactiveUtils.whenOnce(() => !layerView.updating);
            const year = slider.values[0];
            updateRenderer(year);
            updateHistogram(year);
          })
          .catch((e) => {
            console.error(e);
        });

        // Updates the underlying data value driving the expression
        // based on the given year provided by the slider
        function updateRenderer(value) {
          const renderer = layer.renderer.clone();
          const sizeVariable = renderer.visualVariables[0];
          const colorVariable = renderer.visualVariables[1];
          sizeVariable.valueExpression = getSizeValueExpression(value);
          colorVariable.field = "F" + value;
          renderer.visualVariables = [sizeVariable, colorVariable];
          layer.renderer = renderer;
        }

        // Generate color visual variable based on the given year

        function getColorField(value) {
          return "F" + value;
        }

        // Generate size visual variable based on the given year
        // This is the same expression as "size-arcade" above, but
        // modifiable for any given year

        function getSizeValueExpression(value) {
          return `
            var AbsTEMP = Abs($feature.F${value});
            var vs = $view.scale;
            var TempSize = when(
              AbsTEMP > 5, 35,
              AbsTEMP > 4, 30,
              AbsTEMP > 2.5, 25,
              AbsTEMP > 1, 20,
              AbsTEMP > 0.5, 15,
              AbsTEMP > 0.01, 12,
              AbsTEMP < 0.01, 10,
              8
            );
            when(
              vs >=37000000, TempSize,
              vs >=18500000, 2 + TempSize,
              vs >=9300000, 4 + TempSize,
              vs >=4700000, 6 + TempSize,
              vs >=2000000, 8 + TempSize,
              10 + TempSize
            );
          `;
        }

        let histograms = {};
        let histogramChart = null;
        const histMin = -5;
        const histMax = 5;

        let highlight;

        function getAverage(params) {
          return summaryStatistics(params).then((statistics) => {
            return statistics.avg;
          });
        }

        function updateHistogram(year) {
          if (histograms[year]) {
            histogramChart.bins = histograms[year].bins;
            histogramChart.average = histograms[year].average;
            return;
          }

          const params = {
            layer: layer,
            field: "F" + year,
            view: view,
            useFeaturesInView: true,
            numBins: 100,
            minValue: histMin,
            maxValue: histMax
          };

          let average = null;

          return getAverage(params)
            .then((avg) => {
              average = avg;
              return histogram(params);
            })
            .then((histogramResult) => {
              // cache previously used histograms to improve performance
              histograms[year] = {
                bins: histogramResult.bins,
                average: average
              };

              if (!histogramChart) {
                histogramChart = new Histogram({
                  container: "histogram",
                  min: histMin,
                  max: histMax,
                  bins: histogramResult.bins,
                  average: average,
                  dataLines: [
                    {
                      value: 0
                    }
                  ],
                  dataLineCreatedFunction: (element, label, index) => {
                    if (index === 0) {
                      element.setAttribute("y2", "75%");
                    }
                  },
                  labelFormatFunction: (value, type) => {
                    return type === "average" ? value.toFixed(2) + "°" : value;
                  },
                  barCreatedFunction: (index, element) => {
                    const bin = histogramChart.bins[index];
                    const midValue =
                      (bin.maxValue - bin.minValue) / 2 + bin.minValue;
                    const color = getColorFromValue(midValue);
                    element.setAttribute("fill", color.toHex());
                    element.addEventListener("focus", () => {
                      const { minValue, maxValue, count } = bin;
                      const query = lv.layer.createQuery();
                      const field = "F" + slider.values[0];
                      query.where = `${field} >= ${minValue} AND ${field} <= ${maxValue}`;
                      lv.queryObjectIds(query).then((ids) => {
                        if (highlight) {
                          highlight.remove();
                          highlight = null;
                        }
                        highlight = lv.highlight(ids);
                      });
                    });

                    element.addEventListener("blur", () => {
                      if (highlight) {
                        highlight.remove();
                        highlight = null;
                      }
                    });
                  }
                });
              } else {
                histogramChart.bins = histogramResult.bins;
                histogramChart.average = average;
              }
            })
            .catch((e) => {
              console.error(e);
            });
        }

        // Infers the color of the visual variable based on a given value
        // This is used to render and update histogram bars with colors
        // matching the features in the map
        function getColorFromValue(value) {
          const visualVariable = layer.renderer.visualVariables.filter((vv) => {
            return vv.type === "color";
          })[0];
          const stops = visualVariable.stops;
          let minStop = stops[0];
          let maxStop = stops[stops.length - 1];

          let minStopValue = minStop.value;
          let maxStopValue = maxStop.value;

          if (value < minStopValue) {
            return minStop.color;
          }

          if (value > maxStopValue) {
            return maxStop.color;
          }

          const exactMatches = stops.filter((stop) => {
            return stop.value === value;
          });

          if (exactMatches.length > 0) {
            return exactMatches[0].color;
          }

          minStop = null;
          maxStop = null;
          stops.forEach((stop, i) => {
            if (!minStop && !maxStop && stop.value >= value) {
              minStop = stops[i - 1];
              maxStop = stop;
            }
          });

          const weightedPosition =
            (value - minStop.value) / (maxStop.value - minStop.value);

          return Color.blendColors(
            minStop.color,
            maxStop.color,
            weightedPosition
          );
        }

        function updateYearDisplay(year) {
          const yearElement = document.getElementById("yearDiv");
          yearElement.innerText = year;
        }
      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
    <div id="containerDiv" class="geoscene-widget">
      <div class="geoscene-widget">
        <div id="title" class="geoscene-widget">
          <h3>Temperature Anomaly (<span id="yearDiv">1880</span>)</h3>
        </div>
        <div id="histogram" class="geoscene-widget"></div>
        <div class="labels geoscene-widget">
          <span style="float: left">-5° C</span>
          <span style="float: center">0° C</span>
          <span style="float: right">+5° C</span>
        </div>
      </div>
    </div>
    <div id="sliderContainer" class="geoscene-widget">
      <div id="sliderDiv"></div>
    </div>
  </body>
</html>