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

React SVG - zmiana koloru path, kiedy jest ich dużo
Gouda105
  • Rejestracja:prawie 8 lat
  • Ostatnio:około miesiąc
  • Postów:487
0

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

Kopiuj
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.

edytowany 1x, ostatnio: Gouda105
Xarviel
  • Rejestracja:ponad 3 lata
  • Ostatnio:około 9 godzin
  • Postów:847
0

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

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

A nie znacznika img

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

To można manipulować nim przez CSS

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

Kopiuj
svg #super-path {
  fill: #abcdef;
}
edytowany 3x, ostatnio: Xarviel
Gouda105
Tak, ale jeśli będę miał powiedzmy 200 takich ID, które mają zmieniać kolor to powinienem 200 reguł dawać?
Xarviel
Możesz użyć dowolnego selektora CSS i przykladowo zamienić ID na klasy <path class="super-path" ...>
Gouda105
Problem jest taki, że kolor jest generowany na podstawie id.
Xarviel
Jeśli nie można zastosować w tym wypadku innych selektorów, które pozwalają to skrócić jak klasy, albo takie coś np [id*="super"] { fill: #abc; } https://www.w3schools.com/css/css_attribute_selectors.asp to trzeba będzie część rzeczy zdublikować. Możliwe, że dałoby się coś uprościć, ale musiałbym wiedzieć w jaki sposób jest to generowane
Xarviel
@Gouda105 Oczywiście można jeszcze w pewien sposób modyfikować ten obrazek z poziomu JavaScriptu i tutaj coś kombinować, ale powiedziałeś, że wolisz uniknąć ponownego renderowania więc odrzuciłem te opcje na samym początku.
Gouda105
  • Rejestracja:prawie 8 lat
  • Ostatnio:około miesiąc
  • Postów:487
0

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

Kopiuj
<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:

Kopiuj
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:

Kopiuj
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

edytowany 2x, ostatnio: Gouda105
Gouda105
@Xarviel: teraz tak sobię myślę, że może być ponownie renderowane przy zmianie danych wejściowych. Przepraszam za chaotyczność, ale trochę się pogubiłem
Xarviel
  • Rejestracja:ponad 3 lata
  • Ostatnio:około 9 godzin
  • Postów:847
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

Kopiuj
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.

Kopiuj
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.

Kopiuj
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]);
edytowany 6x, ostatnio: Xarviel
Gouda105
Bardzo dziękuję za tak dogłębną odpowiedź. Dostosuję kod pod siebie i wykorzystam.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.