Skip to content

Bản đồ ký hiệu ​

Địa lý

Biểu đồ #20: một bản đồ bong bóng khử chồng lấn bằng mô phỏng lực. Mục tiêu di trú cho MapSymbolForce cũ của sdg-trade - một bản đồ bong bóng chỉ-có-chấm, trong đó tọa độ của mỗi mục được chiếu lên mặt phẳng, sau đó một mô phỏng lực một lần đẩy các vòng tròn chồng lấn ra vừa đủ để dễ đọc, mà không di chuyển chúng xa khỏi vị trí thực. Khác với Bản đồ phân cấp màu, địa lý ở đây là tùy chọn: bỏ qua nó để có diện mạo chỉ-có-chấm của biểu đồ cũ (mặc định), hoặc cung cấp nó để vẽ một nền địa hình mờ nhạt phía sau các ký hiệu.

Example
canvas · responsive
Tìm hiểu thêm: Hướng dẫn Insights·Hướng dẫn DevTools

Một vòng thứ hai đồng tâm (valueSecond) được đọc như một chỉ số con của vòng tròn ngoài - ví dụ "trong đó" một đối tác hoặc kênh cụ thể:

Example
canvas · responsive
Tìm hiểu thêm: Hướng dẫn Insights·Hướng dẫn DevTools

Vị trí chính xác hay tách chồng lấn ​

positionMode quyết định ý nghĩa vị trí của mỗi symbol:

  • "force" (mặc định, giữ tương thích legacy): mô phỏng một lần đẩy các vòng tròn chồng nhau tách ra. Dễ đọc khi nhiều symbol dồn về một chỗ, nhưng symbol có thể lệch khỏi tọa độ thật, và trên biểu đồ nhỏ độ lệch có thể rất lớn.
  • "precise": mỗi symbol nằm đúng tại lng/lat đã chiếu; chấp nhận chồng lấn.

Symbol vẽ nhầm sang nước khác là lỗi chính xác bản đồ, với nhiều đối tượng người xem còn là vấn đề nhạy cảm chính trị. Nên dùng "precise" khi có nền geography: thấy đất liền là người xem sẽ đọc vị trí theo nghĩa đen.

Chuyển sang force để thấy các bong bóng lệch khỏi tọa độ thật nhằm tránh va chạm. Khi có nền bản đồ, precise thường là lựa chọn trung thực hơn.

Bản đồ chỉ để minh họa - và biên giới là vấn đề chính trị

Bản đồ thế giới trên trang này là một tệp GeoJSON đơn giản hóa thuộc phạm vi công cộng, đi kèm ví dụ tài liệu chỉ để minh họa. Nó có thể sai: biên giới, tên gọi và hình dạng KHÔNG có giá trị chính thức, và một số đang bị tranh chấp. Biên giới và tên gọi nào là "đúng" khác nhau theo đối tượng, khu vực pháp lý và tổ chức, và một bản đồ sai ở điểm này là vấn đề chính trị, không phải thẩm mỹ. Không có gì được vẽ ở đây là sự công nhận đối với bất kỳ đường biên giới nào. Trước khi dùng trong sản phẩm, hãy đối chiếu biên giới và tên gọi trong tệp geography bạn truyền vào với chính sách bản đồ của tổ chức: thư viện vẽ tệp nguyên trạng và không sửa gì.

Biểu đồ ở trên là cùng một engine trong mọi framework - chỉ mã tích hợp bên dưới là khác.

Ô lục giác trên lưới tổ ong ​

Đôi khi mọi địa điểm cần có cùng trọng số và giá trị nằm ở màu sắc: một bản đồ ô. Đặt shape: "hexagon" và positionMode: "honeycomb", mỗi ký hiệu trở thành một lục giác cùng kích thước bám vào lưới lục giác: các ô lát kín mặt phẳng, không bao giờ chồng lấn, và ô của một quốc gia ở gần trọng tâm của nó nhất có thể trong lưới. colorScale (miền/dải scaleThreshold, cùng hợp đồng với Bản đồ choropleth) mang giá trị; các ô được tô ở độ mờ đầy đủ vì màu sắc chính là mã hóa.

Example
canvas · responsive
Tìm hiểu thêm: Hướng dẫn Insights·Hướng dẫn DevTools
Example
canvas · responsive
Tìm hiểu thêm: Hướng dẫn Insights·Hướng dẫn DevTools

Cảnh báo về bản đồ minh họa ở trên cũng áp dụng ở đây: các ô nằm trên một bản đồ minh họa, và vị trí của mỗi ô cũng như quốc gia mà nó đại diện chỉ đúng khi geography và tọa độ bạn cung cấp đúng.

  • honeycomb - { radius, gap, orientation, placement, reserve }: bán kính ngoại tiếp của ô tính bằng px (mặc định 11), khoảng cách giữa các cạnh ô (mặc định 2), và đỉnh "flat" (mặc định, cạnh phẳng ở trên) hoặc "pointy". Hướng chi phối cả đường viền lục giác lẫn lưới, để các ô khớp nhau. placement và reserve được mô tả bên dưới.
  • Va chạm (mặc định, honeycomb.placement: "first") được giải quyết theo thứ tự dataSet: ô đến sau mà cell đã bị chiếm sẽ đi ra từng vòng đến cell trống đầu tiên, một cách tất định. Nếu không có cell trống trong sáu vòng, ô sẽ chồng lấn và một onDataWarning loại layout-overflow nêu tên nó.
  • honeycomb.placement: "geographic" giữ các ô lân cận đúng thứ tự thực tế. Ô gần tâm của một ô lưới nhất sẽ giữ ô đó, ô bị đẩy ra nhận ô trống gần vị trí thật của nó nhất, rồi một lượt sửa sẽ hoán đổi hoặc dịch các ô lân cận đang bị đọc ngược đông thành tây hay bắc thành nam. Kết quả không phụ thuộc thứ tự trong dataSet. Nó tốn nhiều công hơn "first" (một lượt sửa sau bước chiếm ô lưới), khoảng 25 ms cho 200 ô. Các cặp lân cận vẫn bị sai thứ tự được đếm trong context (stats.outOfOrderCount, được đo với mọi placement nên có thể so sánh "first"; với "geographic" đó là phần còn lại sau lượt sửa) và chỉ với "geographic" mới được nêu tên trong một onDataWarning loại layout-out-of-order; stats.displacedCount đếm các ô không nằm ở ô lưới gần nhất của mình.
  • honeycomb.reserve: "all" cho cả các ô không có giá trị cũng giữ một ô lưới, nên bố cục không đổi khi giá trị thay đổi (một bản đồ chuyển qua lại giữa các sản phẩm hay các năm). Các ô bị loại bởi disabledItems hoặc radiusVisibleMin vẫn không tham gia bố cục. Mặc định "valued" giữ các ô không có dữ liệu tránh sang một bên.
  • fit: "width" tính bố cục một lần ở kích thước width x height rồi co giãn hình vẽ theo vùng chứa (không bao giờ lớn hơn width), nên các ô không di chuyển khi cửa sổ đổi kích thước. Mặc định "none" vẽ đúng kích thước pixel.
  • Không có giá trị? Bỏ qua value. Mục được tô noDataColor (mặc định #d2d7dd, truyền "transparent" để ẩn), bị loại khỏi thang màu, và, với mặc định honeycomb.reserve: "valued", không bao giờ chiếm cell: ô "không có dữ liệu" không thể đẩy dữ liệu thật đi (honeycomb.reserve: "all" cho chúng cũng giữ một ô lưới, xem bên trên).
  • offset trên một mục dịch chuyển nó theo px sau phép chiếu và trước khi bám lưới: lối thoát cho một nước nhỏ bị nước lớn bên cạnh che khuất.
  • markers vẽ các ghim trang trí phía trên các ô tại một lng/lat (ví dụ trụ sở chính): ghim bản đồ tích hợp, hoặc path của riêng bạn trong hộp pathSize, được co giãn tới size px và neo ở đầu nhọn. Đặt tile cho một marker (id hoặc label của một ô) để ghim nằm trên ô đó, dù bố cục đã đặt ô ở đâu; lng/lat vẫn là phương án dự phòng. markers trong context giữ nguyên lng/lat đã khai báo; với ghim đặt trên một ô, hãy lấy x/y của ô đó từ symbols. Ghim không được bố trí, không nhận hover và không có tooltip; tạo kiểu qua .symbol-map-marker. Ngữ cảnh liệt kê x/y của mọi ký hiệu đã đặt để bạn tự vẽ lớp phủ và chú giải.

shape: "hexagon" cũng dùng được một mình với cách đặt "force" hoặc "precise", và colorScale dùng được với hình tròn: các mảnh ghép kết hợp với nhau.

Phần thuộc về bạn

Thư viện vẽ ô, ghim và màu. Chữ chú giải, nội dung tooltip, định dạng số, nước nào là "nhà" và nhận bảng màu nào thuộc về bên sử dụng: truyền chúng qua colorScale, markers và tooltipFormatter.

Mang theo tọa độ của riêng bạn ​

Khác với biểu đồ cũ (vốn đóng gói một CSV tĩnh khoảng 200 dòng tọa độ quốc gia), @michi-vz/core không đóng gói bảng tọa độ nào. Mỗi mục dataSet tự cung cấp lng/lat riêng:

ts
import { SymbolMapChart } from "@michi-vz/react";

<SymbolMapChart
  dataSet={[
    { id: "usa", label: "United States", lng: -95.7, lat: 38.9, value: 100 },
    { id: "deu", label: "Germany", lng: 13.4, lat: 52.5, value: 60, valueSecond: 30 },
    // ...
  ]}
/>

Vì phạm vi (extent) dùng để khớp bố cục chỉ-có-chấm với vùng vẽ được suy ra từ tọa độ của chính tập dữ liệu của bạn (không phải từ một bảng thế giới luôn đầy đủ được đóng gói sẵn), "thế giới" mà biểu đồ vẽ ra chỉ trải rộng đến mức các điểm bạn truyền vào - xem Ghi chú hành vi bên dưới.

Đặt ký hiệu từ dữ liệu địa lý ​

Với nền geography, bạn có thể bỏ lng/lat: mục sẽ được đặt tại trọng tâm của đối tượng có id trùng với geoId của nó (mặc định: chính id của mục). Các dòng khóa theo mã ISO ghép với tệp bản đồ thế giới khóa cùng cách mà không cần bảng tọa độ nào; mục không khớp với đối tượng nào sẽ bị loại với cảnh báo invalid-geometry nêu rõ id đã tìm.

ts
<SymbolMapChart
  geography={world}
  dataSet={[
    { id: "DEU", label: "Germany", value: 60, valueSecond: 30 },
    { id: "row-9", geoId: "FRA", label: "France", value: 40 },
  ]}
  labelFormatter={(d) => `${d.label} ${Math.round((d.valueSecond! / d.value!) * 100)}%`}
  onSymbolClick={(d) => go(d.id)}
/>

labelFormatter đặt văn bản vẽ bên trong một ký hiệu đủ lớn (mặc định: nhãn), để một tỷ lệ hoặc giá trị có thể đứng cạnh tên; văn bản vẫn được vừa khít với hình tròn. onSymbolClick(item, event) được gọi khi nhấp vào một ký hiệu, trong svg cũng như qua kiểm tra va chạm trên canvas; nhấp chuột vẫn ghim tooltip.

Xem dữ liệu chạy theo năm ​

Gắn date cho từng ký hiệu rồi bật timeline: bản đồ thành câu chuyện theo từng năm với nút play và thanh tua riêng, mỗi lần đặt các ký hiệu của một giai đoạn. Mặc định tắt - không bật thì biểu đồ giữ nguyên.

Bấm nút play dưới biểu đồ: dữ liệu chạy qua từng năm, mỗi lần một snapshot. Kéo thanh tua để nhảy đến năm bất kỳ.

tsx
const ref = useRef<SymbolMapChartHandle>(null);

<SymbolMapChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / seekIndex(i) / stepForward()
vue
<SymbolMapChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" />
svelte
<div use:symbolMapChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>
ts
applySymbolMapChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });
html
<michi-vz-symbol-map-chart id="c"></michi-vz-symbol-map-chart>
<script>
  const el = document.getElementById("c");
  el.timeline = { speedMs: 1000, loop: true };
  // el.getTimeline() -> play() / pause() / seek(year) / seekIndex(i)
</script>
  • speedMs chỉnh nhịp chạy, loop quay vòng, autoplay: true tự chạy khi mount, showControl: false ẩn thanh điều khiển có sẵn.
  • Giá trị trượt mượt giữa các giai đoạn theo mặc định (interpolate); chỉnh chuyển động bằng tweenMs và easing, hoặc đặt interpolate: false để cắt thẳng. Khi bật reduced motion, biểu đồ luôn cắt thẳng.
  • Controller headless luôn sẵn sàng: chart.timeline() cho play() / pause() / toggle() / seek(period) / seekIndex(i) / stepForward() / stepBack(), kèm onStep và formatPeriod trong config khi cần tự dựng UI.
  • seek(period) tìm kỳ trước, so sánh dạng chữ, nên seek(2021) và seek("2021") đều tới 2021 dù ngày trong dữ liệu là số hay chuỗi. Số chỉ được hiểu là vị trí (0 = kỳ đầu) khi không có kỳ nào khớp, còn chuỗi không khớp kỳ nào thì không làm gì. seekIndex(i) luôn đi theo vị trí, giống thanh tua có sẵn.
  • Ký hiệu không có date hiển thị ở mọi giai đoạn.

Cách dùng ​

tsx
import { SymbolMapChart } from "@michi-vz/react";

export default () => <SymbolMapChart {...props} />; // props = các tùy chọn biểu đồ
vue
<script setup>
import { SymbolMapChart } from "@michi-vz/vue";
</script>

<template>
  <SymbolMapChart :options="props" />
</template>
svelte
<script>
  import { symbolMapChart } from "@michi-vz/svelte";
</script>

<div use:symbolMapChart={props}></div>
ts
// main.ts - đăng ký các phần tử một lần
import "@michi-vz/angular";
import { applySymbolMapChartProps } from "@michi-vz/angular";

// component (sử dụng CUSTOM_ELEMENTS_SCHEMA)
// template: <michi-vz-symbol-map-chart #c></michi-vz-symbol-map-chart>
applySymbolMapChartProps(this.c.nativeElement, props);
html
<script type="module" src="https://cdn.jsdelivr.net/npm/@michi-vz/wc/dist/michi-vz-wc.bundle.js"></script>

<michi-vz-symbol-map-chart id="c"></michi-vz-symbol-map-chart>
<script>
  Object.assign(document.getElementById("c"), props); // dataSet, title, …
</script>
ts
import { mountSymbolMapChart } from "@michi-vz/core";

const chart = mountSymbolMapChart(el, props);
chart.update(next);
chart.getContext(); // độc lập với renderer, sẵn sàng cho LLM
chart.destroy();

API ​

Các thuộc tính được định kiểu là SymbolMapChartProps trong @michi-vz/core. Dùng chung cho mọi biểu đồ: width, height, margin, colors / colorsMapping, renderer ("svg", "canvas", hoặc thử nghiệm "webgpu"), highlightItems, disabledItems, và các callback on*. onChartDataProcessed / getContext() trả về ChartContext độc lập với renderer.

Ghi chú hành vi ​

Khử chồng lấn bằng lực một lần ​

lng/lat của mỗi mục được chiếu tới một vị trí pixel "thực"; forceX/forceY ghim mô phỏng vào đúng vị trí đó làm mục tiêu, forceManyBody() thêm một chút tách biệt nhẹ, và forceCollide (bán kính + đệm 2px, 3 lần lặp) mới là thứ thực sự đẩy các vòng tròn chồng lấn ra xa nhau. Mô phỏng ổn định đồng bộ tới CÙNG ngưỡng alpha mà biểu đồ cũ đã dùng (0.0011) - có tính xác định: đầu vào giống nhau luôn ổn định về cùng một bố cục, vì vậy hai lần dựng cùng dữ liệu cho ra vị trí giống hệt nhau đến từng pixel. Hai mục có tọa độ hoàn toàn giống nhau bắt đầu chồng lên nhau và tách ra gọn gàng khi mô phỏng ổn định. positionMode: "precise" bỏ qua hoàn toàn mô phỏng: mỗi symbol giữ nguyên vị trí chiếu chính xác của nó và chấp nhận chồng lấn (xem demo chuyển chế độ ở trên).

Chiếu chỉ-có-chấm so với nền ​

Khi không có geography, projection được chọn (mặc định "geoMercator", giống biểu đồ cũ) được dùng ở dạng chưa tinh chỉnh - một factory chiếu trần trụi không có bất kỳ tinh chỉnh translate/scale/rotate/center nào như Bản đồ phân cấp màu - và phạm vi các điểm đã chiếu sau đó được co giãn lại để lấp đầy vùng vẽ, phản ánh đúng công thức riêng của biểu đồ cũ. Khi cung cấp geography, CÙNG cơ chế điều phối đã tinh chỉnh mà Bản đồ phân cấp màu dùng sẽ thay thế, để nền địa hình và tọa độ ký hiệu chia sẻ một khung địa lý nhất quán (không co giãn phạm vi).

radiusVisibleMin - lọc trên giá trị thô ​

radiusVisibleMin ẩn các mục có value thô bằng hoặc dưới ngưỡng (và, khi valueSecond được đặt, có valueSecond cũng dưới ngưỡng đó) trước khi bố cục lực chạy - được chuyển thể từ bộ lọc riêng của biểu đồ cũ, vốn so sánh giá trị thô, chưa bao giờ so sánh bán kính đã co giãn. Miền của thang bán kính/độ mờ vẫn được xây dựng từ mọi mục có tọa độ hợp lệ bất kể bộ lọc này, để bán kính của một ký hiệu vẫn có thể so sánh được giữa các lần dựng ngay cả khi ngưỡng thay đổi.

Vòng thứ hai đồng tâm ​

valueSecond vẽ một vòng tròn thứ hai, cùng màu với vòng ngoài, ở opacity - 0.3 (giới hạn không âm), vẽ ĐÈ LÊN vòng tròn chính - chuyển thể chính xác từ cách phân lớp riêng của ForceNode cũ. Khi vòng tròn thứ hai nhỏ hơn vòng ngoài, nó đọc như một lõi mờ hơn bên trong một vòng sáng hơn (ví dụ "trong đó"); khi lớn hơn, nó che phủ hoàn toàn vòng tròn ngoài.

Nền địa lý tùy chọn ​

geography (một FeatureCollection GeoJSON hoặc một GeoFeatureItem[] đã chuẩn hóa sẵn, cùng hợp đồng như geography của Bản đồ phân cấp màu) là một khả năng mới - biểu đồ cũ chưa bao giờ vẽ địa hình. Bỏ qua nó để có diện mạo chỉ-có-chấm của bản cũ; cung cấp nó để có một nền mờ nhạt, không tương tác (geographyColor, mặc định #eef1f5) phía sau các ký hiệu.

Kết xuất: SVG, canvas và một WebGPU ủy quyền ​

renderer="svg" (mặc định) vẽ một <circle class="symbol"> cho mỗi mục hiển thị (cộng với một <circle class="symbol-second"> tùy chọn). renderer="canvas" vẽ cùng các ký hiệu đó lên canvas 2D, phân giải màu tô qua cùng một đầu dò CSS phía người dùng mà mọi biểu đồ ký hiệu đơn dùng. renderer="webgpu" ủy quyền cho renderer canvas-2D, cùng lý do như Bản đồ phân cấp màu: nền tùy chọn là GeoJSON tùy ý (có thể lõm, nhiều vòng), nên việc tam giác hóa GPU chính xác là phạm vi không tương xứng ở đây.

Đang tải / không có dữ liệu ​

isLoading và isNodata điều khiển lớp phủ (React: isLoadingComponent / isNodataComponent), giống hệt mọi biểu đồ khác trong hệ thống.

Thẩm quyền màu phía người dùng: ngữ cảnh mang theo legendData ({ label, color, dataLabelSafe }) để một hệ thống màu tiêm CSS có thể khóa các quy tắc theo từng nhãn; onChartDataProcessed chỉ được phát ra khi ngữ cảnh thay đổi.