observable-framework-lib-deckgl

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Library: Deck.gl

库:Deck.gl

Observable Framework documentation: Library: Deck.gl Source: https://observablehq.com/framework/lib-deckgl
deck.gl is a “GPU-powered framework for visual exploratory data analysis of large datasets.” You can import deck.gl’s standalone bundle like so:
js
import deck from "npm:deck.gl";
You can then refer to deck.gl’s various components such as
deck.DeckGL
or
deck.HexagonLayer
. Or for more concise references, you can destructure these symbols into top-level variables:
js
const {DeckGL, AmbientLight, GeoJsonLayer, HexagonLayer, LightingEffect, PointLight} = deck;
The example below is adapted from the documentation.
<div class="card" style="margin: 0 -1rem;">
Observable Framework 文档:库:Deck.gl 来源:https://observablehq.com/framework/lib-deckgl
deck.gl 是一个“基于GPU的大规模数据集可视化探索性数据分析框架。”你可以像这样导入deck.gl的独立包
js
import deck from "npm:deck.gl";
你之后可以引用deck.gl的各类组件,比如
deck.DeckGL
deck.HexagonLayer
。或者为了更简洁的引用,你可以将这些符号解构为顶级变量:
js
const {DeckGL, AmbientLight, GeoJsonLayer, HexagonLayer, LightingEffect, PointLight} = deck;
以下示例改编自官方文档
<div class="card" style="margin: 0 -1rem;">

Personal injury road collisions, 2022

2022年人身伤害道路碰撞事故

${data.length.toLocaleString("en-US")} reported collisions on public roads

${data.length.toLocaleString("en-US")} 起公共道路上报碰撞事故

<figure style="max-width: none; position: relative;"> <div id="container" style="border-radius: 8px; overflow: hidden; background: rgb(18, 35, 48); height: 800px; margin: 1rem 0; "></div> <div style="position: absolute; top: 1rem; right: 1rem; filter: drop-shadow(0 0 4px rgba(0,0,0,.5));">${colorLegend}</div> <figcaption>Data: <a href="https://www.data.gov.uk/dataset/cb7ae6f0-4be6-4935-9277-47e5ce24a11f/road-safety-data">Department for Transport</a></figcaption> </figure> </div>
js
const coverage = view(Inputs.range([0, 1], {value: 0.5, label: "Coverage", step: 0.01}));
const radius = view(Inputs.range([500, 20000], {value: 1000, label: "Radius", step: 100}));
const upperPercentile = view(Inputs.range([0, 100], {value: 100, label: "Upper percentile", step: 1}));
The code powering this example is a bit elaborate. Let’s break it down.
<figure style="max-width: none; position: relative;"> <div id="container" style="border-radius: 8px; overflow: hidden; background: rgb(18, 35, 48); height: 800px; margin: 1rem 0; "></div> <div style="position: absolute; top: 1rem; right: 1rem; filter: drop-shadow(0 0 4px rgba(0,0,0,.5));">${colorLegend}</div> <figcaption>数据来源:<a href="https://www.data.gov.uk/dataset/cb7ae6f0-4be6-4935-9277-47e5ce24a11f/road-safety-data">英国交通部</a></figcaption> </figure> </div>
js
const coverage = view(Inputs.range([0, 1], {value: 0.5, label: "Coverage", step: 0.01}));
const radius = view(Inputs.range([500, 20000], {value: 1000, label: "Radius", step: 100}));
const upperPercentile = view(Inputs.range([0, 100], {value: 100, label: "Upper percentile", step: 1}));
这个示例的实现代码稍复杂,我们来逐步拆解。

1. The data

1. 数据

The accidentology data is loaded as a CSV file, generated by a data loader (
dft-road-collisions.csv.sh
) using DuckDB to produce an extract from the Department for Transport dataset. The country shapes come from a TopoJSON file, which we convert to GeoJSON.
js
const data = FileAttachment("../data/dft-road-collisions.csv").csv({array: true, typed: true}).then((data) => data.slice(1));
const topo = import.meta.resolve("npm:visionscarto-world-atlas/world/50m.json");
const world = fetch(topo).then((response) => response.json());
const countries = world.then((world) => topojson.feature(world, world.objects.countries));
事故数据以CSV文件加载,由数据加载器(
dft-road-collisions.csv.sh
)通过DuckDB从英国交通部的数据集中提取生成。国家边界形状来自TopoJSON文件,我们将其转换为GeoJSON格式。
js
const data = FileAttachment("../data/dft-road-collisions.csv").csv({array: true, typed: true}).then((data) => data.slice(1));
const topo = import.meta.resolve("npm:visionscarto-world-atlas/world/50m.json");
const world = fetch(topo).then((response) => response.json());
const countries = world.then((world) => topojson.feature(world, world.objects.countries));

2. The layout

2. 布局

Using nested divs, we position a large area for the chart, and a card floating on top that will receive the title, the color legend, and interactive controls:
html
<div class="card" style="margin: 0 -1rem;">
我们使用嵌套div为图表设置一个大区域,并在顶部悬浮一个卡片,用于显示标题、颜色图例和交互控件:
html
<div class="card" style="margin: 0 -1rem;">

Personal injury road collisions, 2022

2022年人身伤害道路碰撞事故

${data.length.toLocaleString("en-US")} reported collisions on public roads

${data.length.toLocaleString("en-US")} 起公共道路上报碰撞事故

<figure style="max-width: none; position: relative;"> <div id="container" style="border-radius: 8px; overflow: hidden; background: rgb(18, 35, 48); height: 800px; margin: 1rem 0; "></div> <div style="position: absolute; top: 1rem; right: 1rem; filter: drop-shadow(0 0 4px rgba(0,0,0,.5));">${colorLegend}</div> <figcaption>Data: <a href="https://www.data.gov.uk/dataset/cb7ae6f0-4be6-4935-9277-47e5ce24a11f/road-safety-data">Department for Transport</a></figcaption> </figure> </div> ````
The colors are represented as (red, green, blue) triplets, as expected by deck.gl. The legend is made using Observable Plot:
js
const colorRange = [
  [1, 152, 189],
  [73, 227, 206],
  [216, 254, 181],
  [254, 237, 177],
  [254, 173, 84],
  [209, 55, 78]
];

const colorLegend = Plot.plot({
  margin: 0,
  marginTop: 20,
  width: 180,
  height: 35,
  style: "color: white;",
  x: {padding: 0, axis: null},
  marks: [
    Plot.cellX(colorRange, {fill: ([r, g, b]) => `rgb(${r},${g},${b})`, inset: 0.5}),
    Plot.text(["Fewer"], {frameAnchor: "top-left", dy: -12}),
    Plot.text(["More"], {frameAnchor: "top-right", dy: -12})
  ]
});
<figure style="max-width: none; position: relative;"> <div id="container" style="border-radius: 8px; overflow: hidden; background: rgb(18, 35, 48); height: 800px; margin: 1rem 0; "></div> <div style="position: absolute; top: 1rem; right: 1rem; filter: drop-shadow(0 0 4px rgba(0,0,0,.5));">${colorLegend}</div> <figcaption>数据来源:<a href="https://www.data.gov.uk/dataset/cb7ae6f0-4be6-4935-9277-47e5ce24a11f/road-safety-data">英国交通部</a></figcaption> </figure> </div> ````
颜色以RGB三元组表示,符合deck.gl的要求。图例使用Observable Plot制作:
js
const colorRange = [
  [1, 152, 189],
  [73, 227, 206],
  [216, 254, 181],
  [254, 237, 177],
  [254, 173, 84],
  [209, 55, 78]
];

const colorLegend = Plot.plot({
  margin: 0,
  marginTop: 20,
  width: 180,
  height: 35,
  style: "color: white;",
  x: {padding: 0, axis: null},
  marks: [
    Plot.cellX(colorRange, {fill: ([r, g, b]) => `rgb(${r},${g},${b})`, inset: 0.5}),
    Plot.text(["Fewer"], {frameAnchor: "top-left", dy: -12}),
    Plot.text(["More"], {frameAnchor: "top-right", dy: -12})
  ]
});

3. The DeckGL instance

3. DeckGL实例

We create a DeckGL instance targetting the container defined in the layout. During development & preview, this code can run several times, so we take care to clean it up each time the code block runs:
js
const deckInstance = new DeckGL({
  container,
  initialViewState,
  getTooltip,
  effects,
  controller: true
});

// clean up if this code re-runs
invalidation.then(() => {
  deckInstance.finalize();
  container.innerHTML = "";
});
initialViewState
describes the initial position of the camera:
js
const initialViewState = {
  longitude: -2,
  latitude: 53.5,
  zoom: 5.7,
  minZoom: 5,
  maxZoom: 15,
  pitch: 40.5,
  bearing: -5
};
getTooltip
generates the contents displayed when you mouse over a hexagon:
js
function getTooltip({object}) {
  if (!object) return null;
  const [lng, lat] = object.position;
  const count = object.points.length;
  return `latitude: ${lat.toFixed(2)}
    longitude: ${lng.toFixed(2)}
    ${count} collisions`;
}
effects
defines the lighting:
js
const effects = [
  new LightingEffect({
    ambientLight: new AmbientLight({color: [255, 255, 255], intensity: 1.0}),
    pointLight: new PointLight({color: [255, 255, 255], intensity: 0.8, position: [-0.144528, 49.739968, 80000]}),
    pointLight2: new PointLight({color: [255, 255, 255], intensity: 0.8, position: [-3.807751, 54.104682, 8000]})
  })
];
我们创建一个DeckGL实例,指向布局中定义的容器。在开发和预览阶段,这段代码可能会运行多次,因此我们要确保每次代码块运行时都清理之前的实例:
js
const deckInstance = new DeckGL({
  container,
  initialViewState,
  getTooltip,
  effects,
  controller: true
});

// 代码重新运行时清理实例
invalidation.then(() => {
  deckInstance.finalize();
  container.innerHTML = "";
});
initialViewState
定义了相机的初始位置:
js
const initialViewState = {
  longitude: -2,
  latitude: 53.5,
  zoom: 5.7,
  minZoom: 5,
  maxZoom: 15,
  pitch: 40.5,
  bearing: -5
};
getTooltip
用于生成鼠标悬停在六边形上时显示的内容:
js
function getTooltip({object}) {
  if (!object) return null;
  const [lng, lat] = object.position;
  const count = object.points.length;
  return `纬度: ${lat.toFixed(2)}
    经度: ${lng.toFixed(2)}
    碰撞次数: ${count}`;
}
effects
定义了光照效果:
js
const effects = [
  new LightingEffect({
    ambientLight: new AmbientLight({color: [255, 255, 255], intensity: 1.0}),
    pointLight: new PointLight({color: [255, 255, 255], intensity: 0.8, position: [-0.144528, 49.739968, 80000]}),
    pointLight2: new PointLight({color: [255, 255, 255], intensity: 0.8, position: [-3.807751, 54.104682, 8000]})
  })
];

4. The props

4. 属性

Since some parameters are interactive, we use the
setProps
method to update the layers when their value changes:
js
deckInstance.setProps({
  layers: [
    new GeoJsonLayer({
      id: "base-map",
      data: countries,
      lineWidthMinPixels: 1,
      getLineColor: [60, 60, 60],
      getFillColor: [9, 16, 29]
    }),
    new HexagonLayer({
      id: "heatmap",
      data,
      coverage,
      radius,
      upperPercentile,
      colorRange,
      elevationScale: 50,
      elevationRange: [0, 5000 * t],
      extruded: true,
      getPosition: (d) => d,
      pickable: true,
      material: {
        ambient: 0.64,
        diffuse: 0.6,
        shininess: 32,
        specularColor: [51, 51, 51]
      }
    })
  ]
});
Lastly, the
t
variable controls the height of the extruded hexagons with a generator (that can be reset with a button input):
js
const t = (function* () {
  const duration = 1000;
  const start = performance.now();
  const end = start + duration;
  let now;
  while ((now = performance.now()) < end) yield d3.easeCubicInOut(Math.max(0, (now - start) / duration));
  yield 1;
})();
由于部分参数是交互式的,我们使用
setProps
方法在参数值变化时更新图层:
js
deckInstance.setProps({
  layers: [
    new GeoJsonLayer({
      id: "base-map",
      data: countries,
      lineWidthMinPixels: 1,
      getLineColor: [60, 60, 60],
      getFillColor: [9, 16, 29]
    }),
    new HexagonLayer({
      id: "heatmap",
      data,
      coverage,
      radius,
      upperPercentile,
      colorRange,
      elevationScale: 50,
      elevationRange: [0, 5000 * t],
      extruded: true,
      getPosition: (d) => d,
      pickable: true,
      material: {
        ambient: 0.64,
        diffuse: 0.6,
        shininess: 32,
        specularColor: [51, 51, 51]
      }
    })
  ]
});
最后,
t
变量通过生成器控制六边形的高度(可通过按钮输入重置):
js
const t = (function* () {
  const duration = 1000;
  const start = performance.now();
  const end = start + duration;
  let now;
  while ((now = performance.now()) < end) yield d3.easeCubicInOut(Math.max(0, (now - start) / duration));
  yield 1;
})();