PageSourceSearch

https://www.infoflora.ch/assets/js/taxonomy-graph.js?v2026

js infoflora.ch collected 2026-09-24 08:59:07 UTC 11,579 bytes, 402 lines download raw bytes

1const hpccWasm = window['@hpcc-js/wasm'];
2const dagConfig = {
3  lang: document.documentElement.lang || window?.taxonyms?.lang || 'de',
4  siteUrl: window?.taxonyms?.siteUrl || 'https://www.infoflora.ch',
5  basePurl: window?.taxonyms?.basePurl || 'https://purl.infoflora.ch',
6  obsUrl: window?.taxonyms?.obsUrl || 'https://obs.infoflora.ch',
7  taxonymPath: window?.taxonyms?.taxonymPath || '/rest/public/v5/taxonomy/taxa/'
8};
9const captionMode = {
10  withCaption: 'with-caption',
11  withoutCaption: 'without-caption',
12  captionOnly: 'caption-only'
13}
14
15async function createDagForTaxon(parentElt, taxonId, params) {
16  const {
17    upperRank = 'genus',
18    highlightedReferences = [],
19    scale = 1,
20    w = '100%',
21    h = '100%',
22    padding = 0.25,
23    zoomable = true,
24    centerable = true,
25    interactive = true,
26    highlightNodeColor = 'yellow',
27    highlightEdgeColor = 'red'
28  } = params || {};
29  const opts = {
30    method: 'GET',
31    headers: {}
32  };
33  response = await fetch(getDotUrl(taxonId, captionMode.withoutCaption, upperRank, highlightedReferences), opts);
34  data = await response.text();
35
36  const dot = convertPurlToUrl(data);
37  const ww = Number.isInteger(w) ? w : (parseInt(w) / 100) * parentElt.clientWidth;
38  const wh = Number.isInteger(h) ? h : (parseInt(h) / 100) * parentElt.clientHeight;
39
40  return await buildDag(dot, {
41    scale, w, ww, h, wh, padding, zoomable, centerable, interactive, highlightNodeColor, highlightEdgeColor
42  });
43}
44
45async function createDagCaptionForTaxon(parentElt, taxonId, params) {
46  const {
47    upperRank = 'genus',
48    highlightedReferences = [],
49    scale = 1,
50    w = '100%',
51    h = '100%',
52    zoomable = false
53  } = params || {};
54  const opts = {
55    method: 'GET',
56    headers: {}
57  };
58  response = await fetch(getDotUrl(taxonId, captionMode.captionOnly, upperRank, highlightedReferences), opts);
59  data = await response.text();
60
61  const ww = Number.isInteger(w) ? w : (parseInt(w) / 100) * parentElt.clientWidth;
62  const wh = Number.isInteger(h) ? h : (parseInt(h) / 100) * parentElt.clientHeight;
63  const padding = 0;
64  const centerable = false;
65  const interactive = false;
66
67  return await buildDag(data, {
68    scale, w, ww, h, wh, padding, zoomable, centerable, interactive
69  });
70}
71
72function getDotUrl(taxonId, caption, upperRank, highlightedReferences) {
73  const references = highlightedReferences.join(',');
74
75  return dagConfig.obsUrl
76    + dagConfig.taxonymPath + taxonId
77    + '/relations.dot?highlighted-references='+ references
78    + '&upper-rank=' + upperRank
79    + '&caption=' + caption;
80}
81
82function convertPurlToUrl(str) {
83  let path;
84  const purl = dagConfig.basePurl+'/taxonomy/taxonyms/';
85
86  switch (dagConfig.lang) {
87    case 'fr':
88      path = '/fr/flore/';
89      break;
90    default:
91      path = '/' + dagConfig.lang + '/flora/';
92  }
93
94  return str.replaceAll(purl, dagConfig.siteUrl + path);
95}
96
97buildDag = async (dotStr, params) => {
98  {
99    const {
100      scale, w, ww, h, wh, padding, zoomable, centerable, interactive, highlightNodeColor, highlightEdgeColor
101    } = params || {};
102    const svgStr = await dot2Svg(dotStr);
103    const viz = buildSvg({ svgStr, scale, ww, wh, padding });
104
105    const wrap = d3.create('svg');
106    wrap.attr('class', 'dag-wrapper');
107    wrap.attr('width', viz.wrapper.width);
108    wrap.attr('height', viz.wrapper.height);
109    wrap.attr('style', 'background-color: white;');
110    wrap.html(viz.html);
111
112    const zoomed = e => d3.select(`#${viz.id}`).attr('transform', e.transform);
113    const zoom = d3
114      .zoom()
115      .scaleExtent([scale * 0.05, 5])
116      .on('zoom', zoomed);
117
118    const hover = () => {
119      const nodes = wrap.selectAll('.node');
120      const edges = wrap.selectAll('.edge');
121      const shapes = 'ellipse,polygon,rect';
122      const lines = 'path';
123      let color = {};
124      const clicked = {};
125
126      const select = that => {
127        const s = d3.select(that);
128        const id = s.attr('id');
129        return { s, id };
130      };
131
132      nodes
133        .on('mouseover', function(e) {
134          const { s, id } = select(this);
135          if (!clicked[id]) color[id] = s.selectAll(shapes).attr('fill');
136          s.selectAll(shapes).attr('fill', highlightNodeColor);
137        })
138        .on('mouseout', function(e) {
139          const { s, id } = select(this);
140          if (!clicked[id]) s.selectAll(shapes).attr('fill', color[id]);
141        });
142
143      edges
144        .on("mouseover", function(e) {
145          const { s, id } = select(this);
146          if (!clicked[id]) color[id] = s.selectAll(lines).attr('stroke');
147          s.selectAll(lines)
148            .attr('stroke', highlightEdgeColor)
149            .attr("stroke-width", 2);
150        })
151        .on("mouseout", function(e) {
152          const { s, id } = select(this);
153          if (!clicked[id])
154            s.selectAll(lines)
155              .attr("stroke-width", 1)
156              .attr('stroke', color[id]);
157        });
158
159      /*
160      nodes.on("click", function(e) {
161        const { s, id } = select(this);
162        if (clicked[id]) {
163          s.selectAll(shapes).attr('fill', color[id]);
164          clicked[id] = false;
165        } else {
166          s.selectAll(shapes).attr('fill', highlightNodeColor);
167          clicked[id] = true;
168        }
169      });
170
171      edges.on("click", function(e) {
172        const { s, id } = select(this);
173        if (clicked[id]) {
174          s.selectAll(lines)
175            .attr("stroke-width", 1)
176            .attr('stroke', color[id]);
177          clicked[id] = false;
178        } else {
179          s.selectAll(lines)
180            .attr("stroke-width", 2)
181            .attr('stroke', highlightEdgeColor);
182          clicked[id] = true;
183        }
184      });
185      */
186    };
187
188    const centerOnSelected = () => {
189      const wrapW = parseFloat(wrap.attr('width'));
190      const wrapH = parseFloat(wrap.attr('height'));
191      const targetScale = findScaleToCenter(scale, wrapW, wrapH, viz.graph.width, viz.graph.height);
192      wrap.call(zoom.scaleTo, targetScale);
193
194      const [x, y] = findSelectedTaxonym(wrap);
195      wrap.call(zoom.translateTo, x, y);
196    };
197
198    const fitExtent = () => {
199      const wrapW = parseFloat(wrap.attr('width'));
200      const wrapH = parseFloat(wrap.attr('height'));
201
202      const x = viz.graph.width / 2;
203      const y = -1 * viz.graph.height / 2;
204      const targetScale = findScaleToFit(wrapW, wrapH, viz.graph.width, viz.graph.height);
205
206      wrap.call(zoom.translateTo, x, y);
207      wrap.call(zoom.scaleTo, targetScale);
208    };
209
210    const zoomIn = () => {
211      wrap.transition().call(zoom.scaleBy, 2);
212    };
213
214    const zoomOut = () => {
215      wrap.transition().call(zoom.scaleBy, 0.5);
216    };
217
218    const viewTaxonym = () => {
219      defaultZoom = centerOnSelected;
220      defaultZoom();
221    };
222
223    const viewAll = () => {
224      defaultZoom = fitExtent;
225      defaultZoom();
226    }
227
228    const toggleFullscreen = () => {
229      if (isInFullscreen()) {
230        document.exitFullscreen();
231      } else {
232        div.requestFullscreen();
233      }
234    };
235
236    const isInFullscreen = () => document.fullscreenElement && document.fullscreenElement === div;
237
238    const listenToFullscreen = () => {
239      div.onfullscreenchange = () => {
240        if (document.fullscreenElement) {
241          div.classList.add('fullscreen');
242        } else {
243          div.classList.remove('fullscreen');
244        }
245      }
246    };
247
248    const listenToResize = () => {
249      const relW = (Number.isInteger(w)) ? false : parseInt(w) / 100;
250      const relH = (Number.isInteger(h)) ? false : parseInt(h) / 100;
251
252      d3.select(window).on(`resize.${viz.id}`, () => {
253        let targetWidth, targetHeight;
254        const { width, height } = div.getBoundingClientRect();
255        const fullScreen = isInFullscreen();
256        if (fullScreen) {
257          targetWidth = width;
258          targetHeight = height;
259        } else {
260          targetWidth = relW ? width * relW : Math.min(ww, width);
261          targetHeight = relH ? height * relH : Math.min(wh, height);
262        }
263
264        wrap.attr('width', targetWidth);
265        if (fullScreen || wh !== 0) {
266          wrap.attr('height', targetHeight);
267        }
268
269        defaultZoom();
270      });
271    };
272
273    const buildDom = (zoomable, centerable) => {
274      const dag = d3.create('div').attr('class', 'dag');
275
276      if (zoomable) {
277        const dagZoom = dag.append('div').attr('class', 'dag-zoom');
278        dagZoom.append('button')
279          .attr('class', 'btn zoom-in')
280          .html('+')
281          .on('click', zoomIn);
282        dagZoom.append('button')
283          .attr('class', 'btn zoom-out')
284          .html('-')
285          .on('click', zoomOut);
286
287        dag.append('button')
288          .attr('class', 'btn full-screen')
289          .html('⤢')
290          .on('click', toggleFullscreen);
291      }
292
293      if (centerable) {
294        const dagResize = dag.append('div').attr('class', 'dag-resize');
295        dagResize.append('button')
296          .attr('class', 'btn zoom-reset')
297          .html(lg['flora.fl.view_taxonym'])
298          .on('click', viewTaxonym);
299        dagResize.append('button')
300          .attr('class', 'btn zoom-extent')
301          .html(lg['flora.fl.view_all'])
302          .on('click', viewAll);
303      }
304
305      dag.node().appendChild(wrap.node());
306
307      return dag.node();
308    };
309
310    const div = buildDom(zoomable, centerable);
311    zoomable && wrap.call(zoom);
312    let defaultZoom = centerOnSelected;
313    defaultZoom();
314    listenToResize();
315    listenToFullscreen();
316    interactive && hover();
317
318    return div;
319  }
320}
321
322dot2Svg = async s => {
323  const graphviz = await hpccWasm.Graphviz.load();
324
325  return graphviz.layout(s, 'svg', 'dot');
326}
327
328buildSvg = ({ svgStr, scale, ww, wh, padding }) => {
329  const document = new DOMParser().parseFromString(svgStr, 'image/svg+xml');
330  const elt = document.documentElement;
331  const svg = d3.select(elt).remove();
332
333  const gw = parseInt(elt.getAttribute('width'));
334  const gh = parseInt(elt.getAttribute('height'));
335
336  const buffer = 1 + padding;
337  wh = wh === 0 ? buffer * gh * scale : wh;
338
339  const g = svg.select('#graph0');
340  const transformSrc = g.attr('transform');
341
342  const s = findScaleToCenter(scale, ww, wh, gw, gh);
343  const [x, y] = findSelectedTaxonym(svg);
344  const t = {
345    x: ww / 2 - x * s,
346    y: wh / 2 - y * s
347  };
348
349  g.attr('transform', `translate(${t.x},${t.y}) scale(${s})`);
350
351  const id = `graph-${uuid()}`;
352  g.attr('id', id);
353
354  return {
355    graph: { width: gw, height: gh },
356    wrapper: { width: ww, height: wh },
357    transform: { src: transformSrc, zoom: { scale: s, translate: t } },
358    id,
359    html: svg.html()
360  };
361}
362
363function findScaleToFit(wrapWidth, wrapHeight, graphWidth, graphHeight) {
364  return Math.min(wrapWidth / graphWidth, wrapHeight / graphHeight);
365}
366
367function findScaleToCenter(initialScale, wrapWidth, wrapHeight, graphWidth, graphHeight) {
368  const scaleToFit = findScaleToFit(wrapWidth, wrapHeight, graphWidth, graphHeight);
369
370  return Math.max(initialScale, scaleToFit);
371}
372
373function findSelectedTaxonym(svg) {
374  let polygon = svg.select('g polygon[fill="#e4e4e4"]');
375  if (polygon.empty()) {
376    polygon = svg.select('g polygon');
377  }
378  const points = bbox(polygon);
379  const x = (points.xMin + points.xMax) / 2;
380  const y = (points.yMin + points.yMax) / 2;
381
382  return [x, y];
383}
384
385bbox = polygon => {
386  let x, y, xList = [], yList = [];
387
388  for (point of polygon.attr('points').split(' ')) {
389    [x, y] = point.split(',');
390    xList.push(x);
391    yList.push(y);
392  }
393
394  return {
395    xMin: Math.min(...xList),
396    xMax: Math.max(...xList),
397    yMin: Math.min(...yList),
398    yMax: Math.max(...yList)
399  }
400};
401
402uuid = () => String(Math.floor(Math.random() * 1e9));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.