import { geoPath, geoIdentity } from "d3-geo";
const d3 = Object.assign({}, { geoPath, geoIdentity });
import { create } from "../container/create";
import { render } from "../container/render";
import { tooltip } from "../helpers/tooltip";
import { random } from "../tool/random";
import { interpolate } from "flubber"; // đ Flubber
import {
camelcasetodash,
unique,
implantation,
propertiesentries,
detectinput,
check,
getsize,
} from "../helpers/utils";
/**
* @function path
* @description Generates SVG paths from a GeoJSON. Supports transitions like circle mark. Adds a layer to SVG and returns its id or rendered SVG if new container.
* @see {@link https://observablehq.com/@neocartocnrs/path-mark}
*
* @property {object} data - GeoJSON FeatureCollection for iteration
* @property {object} datum - GeoJSON FeatureCollection for a single datum
* @property {string} [id] - layer id
* @property {string} [coords="geo"] - "svg" if coordinates are already in SVG plane
* @property {boolean} [clip=true] - clip paths with outline
* @property {string|function} [fill] - fill color
* @property {string|function} [stroke] - stroke color
* @property {number} [strokeWidth=1] - stroke width
* @property {boolean|function} [tip=false] - tooltip function
* @property {boolean} [view=false] - viewof for Observable
* @property {object} [tipstyle] - tooltip style
* @property {boolean} [transition=false] - enable transition for updates
* @property {number} [duration=500] - transition duration in ms
* @property {*} [*] - other SVG attributes
* @property {*} [svg_*] - SVG container options if new container
*/
export function path(arg1, arg2) {
const newContainer = !arg2 && !arg1?._groups;
arg1 = newContainer && !arg1 ? {} : arg1;
arg2 = arg2 || {};
const options = {
mark: "path",
id: unique(),
coords: "geo",
clip: true,
strokeWidth: 1,
clipPath: undefined,
fill: undefined,
stroke: undefined,
tip: false,
tipstyle: undefined,
view: false,
transition: false,
duration: 500,
before: null,
after: null,
};
const opts = { ...options, ...(newContainer ? arg1 : arg2) };
const svgOpts = { domain: opts.data || opts.datum };
Object.keys(opts)
.filter((k) => k.startsWith("svg_"))
.forEach((k) => {
svgOpts[k.slice(4)] = opts[k];
delete opts[k];
});
const svg = newContainer ? create(svgOpts) : arg1;
if (opts.data || opts.datum) svg.data = true;
const randomCol = random();
if (opts.data) {
if (implantation(opts.data) === 2) {
opts.fill ??= "none";
opts.stroke ??= randomCol;
} else {
opts.fill ??= randomCol;
opts.stroke ??= "white";
}
}
if (opts.datum) {
if (implantation(opts.datum) === 2) {
opts.fill ??= "none";
opts.stroke ??= randomCol;
} else {
opts.fill ??= randomCol;
opts.stroke ??= "none";
}
}
let layer = svg.select(`#${opts.id}`);
if (layer.empty()) {
const before = opts.before
? opts.before.startsWith("#")
? opts.before
: `#${opts.before}`
: null;
const after = opts.after
? opts.after.startsWith("#")
? opts.after
: `#${opts.after}`
: null;
if (before && svg.select(before).node()) {
layer = svg
.insert("g", before)
.attr("id", opts.id)
.attr("data-layer", "path");
} else if (after && svg.select(after).node()) {
const ref = svg.select(after).node();
layer = svg.append("g").attr("id", opts.id).attr("data-layer", "path");
ref.after(layer.node());
} else {
layer = svg.append("g").attr("id", opts.id).attr("data-layer", "path");
}
}
if (!opts.transition) layer.selectAll("*").remove();
if (svg.zoomable && !svg.parent) {
const existingIndex = svg.zoomablelayers.findIndex((d) => d.id === opts.id);
if (existingIndex < 0) {
svg.zoomablelayers.push({
mark: opts.mark,
id: opts.id,
coords: opts.coords,
});
} else {
svg.zoomablelayers[existingIndex] = {
mark: opts.mark,
id: opts.id,
coords: opts.coords,
};
}
}
const projection =
opts.coords === "svg"
? d3.geoIdentity().scale(svg.zoom.k).translate([svg.zoom.x, svg.zoom.y])
: svg.projection;
const pathGen = d3.geoPath(projection);
if (opts.clipPath) {
layer.attr("clip-path", opts.clipPath);
opts.clip = false;
} else if (opts.clip && opts.coords !== "svg" && svg.initproj !== "none") {
const clipid = unique();
svg
.append("clipPath")
.attr("id", clipid)
.append("path")
.attr("d", pathGen({ type: "Sphere" }));
layer.attr("clip-path", `url(#${clipid})`);
}
const entries = Object.keys(opts);
const notSpecificAttr = entries.filter(
(key) =>
![
"mark",
"id",
"datum",
"data",
"coords",
"tip",
"tipstyle",
"clipPath",
].includes(key)
);
const fields = propertiesentries(opts.data || opts.datum);
const layerAttr = notSpecificAttr.filter(
(key) => detectinput(opts[key], fields) === "value"
);
layerAttr.forEach((key) => layer.attr(camelcasetodash(key), opts[key]));
const eltAttr = notSpecificAttr.filter((key) => !layerAttr.includes(key));
eltAttr.forEach((key) => (opts[key] = check(opts[key], fields)));
// --- DATUM HANDLING ---
if (opts.datum) {
const datumFields = propertiesentries([opts.datum]);
let datumPath = layer.selectAll("path").data([opts.datum]);
datumPath.join(
(enter) =>
enter
.append("path")
.attr("d", pathGen)
.style(
"fill",
typeof opts.fill === "function" ? opts.fill(opts.datum) : opts.fill
)
.style(
"stroke",
typeof opts.stroke === "function"
? opts.stroke(opts.datum)
: opts.stroke
)
.style("opacity", opts.transition ? 0 : 1)
.call(
(enter) =>
opts.transition &&
enter.transition().duration(opts.duration).style("opacity", 1)
),
(update) =>
update.call((update) =>
opts.transition
? update
.transition()
.duration(opts.duration)
.attrTween("d", function (d) {
const from = this.getAttribute("d") || pathGen(d);
const to = pathGen(d);
return interpolate(from, to);
})
.style(
"fill",
typeof opts.fill === "function"
? opts.fill(opts.datum)
: opts.fill
)
.style(
"stroke",
typeof opts.stroke === "function"
? opts.stroke(opts.datum)
: opts.stroke
)
: update
.attr("d", pathGen)
.style(
"fill",
typeof opts.fill === "function"
? opts.fill(opts.datum)
: opts.fill
)
.style(
"stroke",
typeof opts.stroke === "function"
? opts.stroke(opts.datum)
: opts.stroke
)
),
(exit) => exit.remove()
);
// Tooltip attachĂ© au body pour ĂȘtre au-dessus
if (opts.tip || opts.tipstyle || opts.view) {
tooltip(
null,
[opts.datum],
svg,
opts.tip,
opts.tipstyle,
datumFields,
opts.view
);
}
}
// --- DATA HANDLING ---
if (opts.data) {
const features = opts.data.features.filter((d) => d.geometry !== null);
const paths = layer
.selectAll("path")
.data(features, (d, i) => d.id || d.properties?.id || i);
paths
.exit()
.transition()
.duration(opts.duration)
.style("opacity", 0)
.remove();
if (opts.transition) {
paths
.transition()
.duration(opts.duration)
.attrTween("d", function (d) {
const from = this.getAttribute("d") || pathGen(d);
const to = pathGen(d);
const interpolator = interpolate(from, to);
return (t) => interpolator(t);
})
.style("fill", (d) =>
typeof opts.fill === "function" ? opts.fill(d) : opts.fill
)
.style("stroke", (d) =>
typeof opts.stroke === "function" ? opts.stroke(d) : opts.stroke
);
} else {
paths
.attr("d", pathGen)
.style("fill", (d) =>
typeof opts.fill === "function" ? opts.fill(d) : opts.fill
)
.style("stroke", (d) =>
typeof opts.stroke === "function" ? opts.stroke(d) : opts.stroke
);
}
paths
.enter()
.append("path")
.attr("d", pathGen)
.style("opacity", opts.transition ? 0 : 1)
.style("fill", (d) =>
typeof opts.fill === "function" ? opts.fill(d) : opts.fill
)
.style("stroke", (d) =>
typeof opts.stroke === "function" ? opts.stroke(d) : opts.stroke
)
.transition()
.duration(opts.duration)
.style("opacity", 1);
if (opts.tip || opts.tipstyle || opts.view) {
tooltip(
layer,
opts.data,
svg,
opts.tip,
opts.tipstyle,
fields,
opts.view
);
}
}
Object.assign(svg, { viewbox: getsize(layer) });
if (newContainer) {
const size = getsize(layer);
svg
.attr("width", size.width)
.attr("height", size.height)
.attr("viewBox", [size.x, size.y, size.width, size.height]);
return render(svg);
} else {
return `#${opts.id}`;
}
}