Skip to content

JavaScript and D3

OpenSoil's CORS policy supports public, credential-free browser reads. Pedon discovery returns GeoJSON directly. The mapped-survey location response remains ordinary JSON because it can contain many horizon/property observations at one coordinate.

Open the live pedon map

Map observed NCSS pedons

<svg id="pedon-map" width="720" height="420" aria-label="Observed NCSS laboratory pedons"></svg>
<script type="module">
  import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

  const params = new URLSearchParams({
    bbox: "-112.5,39.5,-111.5,40.0",
    limit: "100",
  });
  const response = await fetch(`https://api.opensoil.net/v1/pedons?${params}`);
  if (!response.ok) throw new Error(`OpenSoil returned ${response.status}`);
  const collection = await response.json();

  const svg = d3.select("#pedon-map");
  const projection = d3.geoMercator().fitExtent(
    [[30, 30], [690, 390]],
    collection,
  );

  svg.selectAll("circle")
    .data(collection.features)
    .join("circle")
    .attr("cx", feature => projection(feature.geometry.coordinates)[0])
    .attr("cy", feature => projection(feature.geometry.coordinates)[1])
    .attr("r", 5)
    .attr("fill", "#8b5e3c")
    .append("title")
    .text(feature =>
      `${feature.properties.soil_name ?? "Unnamed soil"} · pedon ${feature.properties.pedon_key}`,
    );
</script>

Use the feature's pedon_key with /v1/pedons/{pedon_key} only after the user selects a point. This avoids downloading horizon observations for every map marker. The collection's truncated field indicates that the bounding box must be narrowed to see beyond the requested limit.

Plot a queried point

<svg id="soil-map" width="720" height="420" aria-label="Queried soil location"></svg>
<script type="module">
  import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

  const location = { latitude: 39.7102, longitude: -111.8363 };
  const params = new URLSearchParams({
    ...location,
    properties: "ph",
    depth_top_cm: "0",
    depth_bottom_cm: "30",
  });
  const response = await fetch(
    `https://api.opensoil.net/v1/soil/location?${params}`,
  );
  if (!response.ok) throw new Error(`OpenSoil returned ${response.status}`);
  const result = await response.json();

  const observation = result.observations.find(
    (item) => item.property.id === "ph",
  );
  if (!observation) throw new Error("No pH value was reported for this interval");

  const feature = {
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [location.longitude, location.latitude],
    },
    properties: {
      value: observation.value,
      unit: observation.unit,
      dataKind: observation.data_kind,
      provider: observation.source.provider,
      warning: observation.warnings[0],
    },
  };

  const svg = d3.select("#soil-map");
  const projection = d3.geoAlbersUsa().fitExtent(
    [[30, 30], [690, 390]],
    { type: "FeatureCollection", features: [feature] },
  );
  const [x, y] = projection(feature.geometry.coordinates);

  svg.append("circle")
    .attr("cx", x)
    .attr("cy", y)
    .attr("r", 9)
    .attr("fill", d3.scaleSequential([4, 9], d3.interpolateRdYlBu)(feature.properties.value));
  svg.append("text")
    .attr("x", x + 14)
    .attr("y", y + 5)
    .text(`pH ${feature.properties.value} · ${feature.properties.dataKind}`);
</script>

For a real national map, supply your own basemap geometry and projection bounds. A single point cannot meaningfully fit a national projection on its own.

Visualization rules

  • Never plot all horizon rows as if they were independent locations.
  • Never color an entire search area with a pedon value; every feature is a sampled point.
  • Choose and label a depth interval.
  • Decide how multiple mapped components are represented; do not treat component percent as point probability.
  • Keep data_kind visible in legends or tooltips.
  • Display provider and retrieval time.
  • Do not suppress warnings because they are inconvenient for the chart.
  • Avoid cross-provider color scales until units, methods, depths, and support are compatible.

SSURGO polygon geometries and bulk map tiles remain roadmap work. Pedon GeoJSON is suitable for bounded discovery and small dashboards, not national bulk extraction or high-volume tiling.