React SVG - zmiana koloru path, kiedy jest ich dużo

0

Witam,
mam svg, które zawiera około 1000 potomków path. Importuję je za pomocą

import { ReactComponent as SvgMap } from './ww.svg';
...
return(
  <SvgMap/>
);

i teraz chciałbym zmienić w jednym z path o odpowiednim ID jego fill kolor. Jak mogę to zrobić?

Jeszcze dodam, że najlepiej gdyby nie wiązało się to z ponownym renderowaniem SVG.

0

Jeśli ten <SvgMap /> renderuje się do postaci standardowego svg

<svg ...>
  <path id="super-path" ...>
  ...
</svg>

A nie znacznika img

<img src="obrazek.svg" ...>

To można manipulować nim przez CSS

https://css-tricks.com/svg-properties-and-css/

svg #super-path {
  fill: #abcdef;
}
0

@Xarviel: mam svg, które przedstawia województwa. Tutaj przykładowy path z niego:

<path
     d="M 116.66556,371.52392 C 115.23145,370.08289 [...] "
     id="02" />

Skróciłem go, żeby nie zajął pół strony. Jest takich 16. ID odpowiada kodowi TERYT, służącego do oznaczania terytorium.

Następnie jest wyświetlane:

import { ReactComponent as SvgMap } from './ww.svg';
...
return(
  <SvgMap/>
);

Użytkownik wpisuje kilka kodów TERYT do textarea oraz odpowiadające im wartości, na podstawie czego generuję odpowiedni kolor dla województwa:

for(let i=0; i<entries.length; i++){
   let percent = (entries[1][i] / maxValue);
   let color = `rgba(255, 0, 0, ${percent})`;
 }

gdzie enteries[1] to wartość aktualnie iterowanego "wpisu" wg. wzoru [TERYT: value] np. {02: 4095}.
I teraz chciałbym kolor ze zmiennej color nadać odpowiedniemu path, które ma id zgodne z aktualnie iterowanym kodem TERYT.

Całe SVG to ten plik, tylko z podmienionymi id na kody TERYT i usuniętymi znacznikami XML.
https://upload.wikimedia.org/wikipedia/commons/b/bf/POL_location_map.svg

1

Zrobiłem przykład online https://stackblitz.com/edit/react-c1rcyu?file=src/App.js (jakby link jakoś nie działał to daj znać).

W swoim przykładzie zrobiłem coś takiego, że cały obrazek SVG wyeksportowałem do osobnego komponentu i otoczyłem go funkcją forwardRef i użyłem useRef, żeby móc go pobrać i zmieniać odpowiednie atrybuty. Dodatkowo musiałem zmienić zapis atrybutu style, bo React się czepiał, że wartość nie jest obiektem style={{ fill: '#94add6', fillOpacity: 1 }}. Dodałem im jakieś ID, ale równie dobrze mogłaby to być klasa, albo jakikolwiek inny atrybut, który będzie unikatowy (tylko taka drobna uwaga, że w tym przykładzie online numeracja województw jest trochę upośledzona, bo w którymś zrobiłem literówkę i wyszło mi finalnie 17 zamiast 16 :D :D)

https://pl.reactjs.org/docs/forwarding-refs.html
https://pl.reactjs.org/docs/hooks-reference.html#useref

const Poland = forwardRef((props, ref) => {
  return (
    <svg
      ref={ref}
      ...
    >
        <path
          ...
          style={{ fill: '#94add6', fillOpacity: 1 }}
          id="territory-1"
        />

        ...
      />
    </svg>
 );
};

export default Poland;

W drugim komponencie jest prosta textarea i nasza mapka

Zrzut ekranu z 2022-07-06 15-38-13.png

I dla uproszczenia cały mechanizm wrzuciłem do jednego komponentu, ale można go powydzielać do osobnych funkcji / hooków.

import React, { useState, useRef, useEffect } from 'react';
import Poland from './poland.js';

const App = () => {
  const [territories, setTerritories] = useState('');
  const polandRef = useRef();

  useEffect(() => {
    const territoriesIds = territories
      .split(',')
      .map((el) => el.trim())
      .filter((el) => el);

    for (const path of polandRef.current.querySelectorAll('path')) {
      if (territoriesIds.some((tId) => `territory-${tId}` === path.id)) {
        if (!path.dataset.color) {
          const hue = Math.random() * 100;
          const saturation = Math.random() * 100;
          const lightness = Math.random() * 100;
          const color = `hsl(${hue}, ${saturation}%, ${lightness}%)`;

          path.dataset.color = color;
        }

        path.style.fill = path.dataset.color;
      } else {
        path.style.fill = '#94add6';
      }
    }
  }, [territories]);

  const transormTerritories = (e) => {
    const values = e.target.value.replace(/,+/, ',');

    setTerritories(values);
  };

  return (
    <>
      <textarea onChange={transormTerritories} value={territories}></textarea>

      <Poland ref={polandRef} />
    </>
  );
};

export default App;

MetodatransormTerritories waliduje tekst wpisany przez użytkownika (usuwa nadmiarowe przecinki :D :p)

Na samym początku hooka useEffect robię proste przekształcenie wartości pobranych od użytkownika w tablicę ID i pobieram wszystkie znaczniki path z svg. Jeśli konkretny path posiada wartość z tablicy to losuje mu kolor i przypisuje go do customowego atrybutu dataset, oraz ustawiam odpowiednią wartość fill. W przypadku gdy nie ma takiego id to ustawiam standardowy kolor, który był na samym początku.

Walidacja pola textarea jest trochę słaba, bo pewnie można znaleźć więcej rzeczy, niż usuwanie przecinków i nadmiarowych spacji, ale starałem się uprościć wszystko do maksimum.

EDIT:

Jeszcze wpadłem na pomysł, że można byłoby zmienić lekko generowanie kolorów. Na samym początku losujemy wszystkie kolory, których będziemy używać i później jedynie przypisujemy wylosowany kolor.

useEffect(() => {
  for (const path of polandRef.current.querySelectorAll('path')) {
    const hue = Math.random() * 100;
    const saturation = Math.random() * 100;
    const lightness = Math.random() * 100;
    const color = `hsl(${hue}, ${saturation}%, ${lightness}%)`;

    path.dataset.color = color;
  }
}, [polandRef.current]);

useEffect(() => {
  const territoriesIds = territories
    .split(',')
    .map((el) => el.trim())
    .filter((el) => el);

  for (const path of polandRef.current.querySelectorAll('path')) {
    path.style.fill = territoriesIds.some((tId) => `territory-${tId}` === path.id)
      ? path.dataset.color
      : '#94add6'
  }
}, [territories]);

1 użytkowników online, w tym zalogowanych: 0, gości: 1