在反应中重新渲染 d3 图

问题描述

我正在生成一个包含音乐数据的 d3 极坐标图,并正在按名称过滤搜索。我在自己的组件中有一个文本输入和提交按钮,现在只想按艺术家姓名过滤我的对象。

我尝试了两种方法。一种使用 apollo 客户端缓存并在提交时更新缓存(我不确定我是否正确实施)另一种方法是在 d3 对象中编写事件侦听器以运行函数过滤器并重新渲染图形。我通过使用复选框触发 initvis 函数来通过键签名过滤我的数据并重新渲染页面上的图形(有效),从而执行了类似于重新渲染的操作。 0 是次要的 1 是主要的。截至目前,该图无法重新渲染。 graph.js 中的数据似乎不会随着 apollo 发生变异,而且我尝试过滤的方式也没有给我任何结果。谢谢!

searchbar.js

import styles from './searchbar.module.css';

import { useQuery,gql } from '@apollo/client';
import axios from 'axios';

import { songsVar } from '../lib/apolloClient'
import { setInfo } from '../redux/actions/main.js'
import { tracksVar } from '../pages/index'

const myQuery = gql`
query getsongs ($artist: String) {
  songs (filter: { artists: {contains: $artist }}) {
    name
    artists
    key
    mode
    tempo
    releaseYear
  }
}
`


export default function Searchbar({ placeholder,initialApolloState }) {
  // const songs = songsVar()
  // console.log('initialApolloState in searchBar',initialApolloState())
  const [searchQuery,setSearchQuery] = useState('')
  const { loading,error,data,refetch,client } = useQuery(myQuery,{
    variables: {artist: searchQuery}
  });
  // if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :</p>;
    
    const onChange = (e) => {
      e.preventDefault();
      setSearchQuery(e.target.value);
      console.log('songsvar on form change -->',songsVar())
    }
    
    const handleClick = async (e) => {
      e.preventDefault();
      songsVar(searchQuery)
      tracksVar(searchQuery)
      console.log('data in SearchBar -->',data)
      console.log('songsvar data in SearchBar -->',songsVar())
      console.log('tracksVar data in SearchBar -->',tracksVar())
    
    }

  //GraphQL query
  return (
    <div>
      <div className="field has-addons">
        <div className="control">
          <input
            className="input"
            data-testid="searchInput"
            type="text"
            id="qInput"
            placeholder={placeholder}
            onChange={onChange}
            value={searchQuery} 
          />
        </div>
        <div className="control ">
          <button className="button" id="aeriaSearchButton" onClick={handleClick}>
            {/* <FontAwesomeIcon icon={faSearch} /> */}S
          </button>
        </div>
      </div>
    </div>
  );
}

graph.js(反应组件)

import { useQuery,gql } from '@apollo/client';

import axios from 'axios'
import styles from './graph.module.css';
import D3Component from '../lib/d3display';
import { songsVar } from '../lib/apolloClient'
import { tracksVar } from '../pages/index.js'

let vis;


export default function Graph({initialApolloState}) {
    
    const songsQuery = gql`
    query allSongs{
        songs {
            name
            artists
            key
            mode
            tempo
            releaseYear
        }
    }`

    const { data } = useQuery(songsQuery);
    const currSongsVar = songsVar(data)
    const currSongsVar2 = tracksVar()

    // const [graphData,setGraphData] = useState(currSongsVar2);
    
    const [width,setWidth] = useState(600);
    const [height,setHeight] = useState(600);
    const [active,setActive] = useState(false);
    const [keySig,setKeySig] = useState('Minor');
    const [bgColor,setBgColor] = useState('#360071');
    const [textColor,setTextColor] = useState('beige');
    const [lineColor,setLineColor] = useState('beige');
    const [artist,setArtist] = useState('');
    const [zoomState,setCurrentZoomState] = useState();
    
    const refElement = useRef(null);
    
    useEffect(handleResizeEvent,[]);
    useEffect(initVis,[songsVar(),artist,active]);
    useEffect(updateVisOnResize,[width,height]);
    
    function majorOrMinor() {
      if (active === false) {
          setBgColor('beige') 
          setActive(true)
          setKeySig('Major')
          setTextColor('black')
          setLineColor('beige')
        }
      else {
          setBgColor('#360071') ;
          setActive(false);
          setKeySig('Minor');
          setTextColor('beige')
          setLineColor('#360071')
        } 
    }

    function handleResizeEvent() {
        let resizeTimer;
        const handleResize = () => {
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(function () {
                setWidth(width);
                setHeight(height);
            },300);
        };
        window.addEventListener('resize',handleResize);

        return () => {
            window.removeEventListener('resize',handleResize);
        };
    }

    function initVis() {
        setArtist(tracksVar())
        if (currSongsVar && currSongsVar.songs.length) {
            songsVar(songsVar())
            const d3Props = {
                data: currSongsVar.songs,width,height,textColor,backgroundColor: bgColor,lineColor,artist: currSongsVar2,onDatapointClick: setActive
            };
            vis = new D3Component(refElement.current,d3Props);
            
        }
        else console.log('no data')
    }

    function updateVisOnResize() {
        vis && vis.resize(width,height);
    }

    return (
        <div className='react-world'>
            {/* <div>{keySig}</div> */}
            <input type="checkBox" className={styles.checkBox} id="checkBox" name="major/minor" value="major/minor" checked={active} onChange={majorOrMinor}></input>
            <div id="d3Graph" key={new Date().getTime()} ref={refElement} />
        </div>
    );
}

d3graph.js

import { useState } from 'react';
import * as d3 from 'd3';
import { entries,text,zoomTransform } from 'd3';

export default class D3display {
  containerEl;
  props;
  svg;

  constructor(containerEl,props) {
    this.containerEl = containerEl;
    this.props = props;
    const { width,backgroundColor} = props;

    
    this.svg = d3
    .select(containerEl)
    .append('svg')
    .style('background-color',backgroundColor)
    .attr('id','d3Canvas')
    .attr('width',width + 20 + 20)
    .attr('height',height + 20 + 20)
    .attr('class','radar' + containerEl);
    this.updateDatapoints();
  }
  

  updateDatapoints = () => {
    //deconstructs variable
    const {
      svg,props: { data,artist},} = this;
    const keysToString = {
      8: 'c',3: 'c#',10: 'd',5: 'd#',0: 'e',7: 'f',2: 'f#',9: 'g',4: 'g#',11: 'a',6: 'a#',1: 'b',};

    const apiNotesToWheelNotes = {
      0: 8,1: 3,2: 10,3: 5,4: 0,5: 7,6: 2,7: 9,8: 4,9: 11,10: 6,11: 1,};
    let transform;
    const zoomBehavior = d3.zoom()
      .scaleExtent([1,5])
      .translateExtent([[0,0],height]])
      .on('zoom',() => {
        console.log('zoomed')
        const zoomState = zoomTransform(svg.node())
        //set current zoom state
        console.log('zoomState -->',zoomState)
        transform = d3.event.transform;
        console.log('transform',transform);
        // svg.attr('transform',transform.toString());
        
      });
    svg.call(zoomBehavior);
    //EVENT HANDLER FOR CHECKBox
    d3.select("#checkBox").on('change',modeChange) 
    //EVENT HANDLER FOR Searchbar
    d3.select('#aeriaSearchButton').on('click',filtergraph)


    let radius = 200;
    let maxValue = 200;
    // let maxValue = Math.max(0,d3.max(data,(i) => d3.max(i.map(o => o.value))))
    // All songs/rows by key
    let allAxis = data.map((i,j) => i['key']);
    const removeDuplicates = (inputArr) => {
      let uniqueVals = {};
      return inputArr.filter((el) => {
        return uniqueVals.hasOwnProperty(el) ? false : (uniqueVals[el] = true);
      });
    };
    //select tempos
    let allTempos = data.map((i,j) => i['tempo']);

    // numRange creates 0-11 range of notes from data
    let numRange = Object.keys(removeDuplicates(allAxis));
    let total = numRange.length;
    // let radius = Math.min(width / 2,height / 2)
    let angleSlice = (Math.PI * 2) / total;
    //scale for radius and tempo
    let rScale = d3.scaleLinear().range([0,radius]).domain([0,maxValue]);

    //mode range will always be constant
    // g creates the area to draw on
    let g = svg
      .append('g')
      .attr('transform',`translate(${width / 2},${height / 2})`)
      // .attr('transform',"translate(" + transform + ")" + " scale(" + transform + ")");
    

    //circular grid
    let axisGrid = g.append('g').attr('class','axisWrapper');
    axisGrid
      .selectAll('.levels')
      .data(d3.range(1,5).reverse())
      .join('circle')
      .attr('class','gridCircle')
      .attr('r',(d,i) => (radius / 4) * d)
      .style('fill','#CDCDCD')
      .style('stroke','#CDCDCD')
      .style('fill-opacity',0.1);

    //text indicating bpm
    axisGrid
      .selectAll('.axisLabel')
      .data(d3.range(1,5))
      .join('text')
      .attr('class','axisLabel')
      .attr('x',(d) => (d * radius) / 4)
      .attr('y',4 * Math.sin(angleSlice / 20 - Math.PI / 2))
      .attr('dy','0.1em')
      .style('font-size','10px')
      .attr('fill','#737373')
      .text((d,i) => {
        return (200 * d) / 4 + 'bpm';
      });

    //create key line points from center
    //REMOVE DUPLICATES from DATA pull filter range to be 1[7]
    let axis = axisGrid
      .selectAll('.axis')
      .data(numRange) // [0-11]
      .join('g')
      .attr('class','axis');
    // //append the lines
    axis
      .append('line')
      .attr('x1',0)
      .attr('y1',0)
      .attr(
        'x2',i) =>
          rScale(maxValue * 1.1) * Math.cos(angleSlice * i - Math.PI / 2)
      )
      .attr(
        'y2',i) =>
          rScale(maxValue * 1.1) * Math.sin(angleSlice * i - Math.PI / 2)
      )
      .attr('class','line')
      .attr('stroke',lineColor)
      .attr('stroke-width','2px');
    //create key names on each line
    axis
      .append('text')
      .attr('class','legend')
      .style('font-size','11px')
      .style('fill',textColor)
      .attr('text-anchor','middle')
      .attr('dy','0.35em')
      .attr(
        'x',i) =>
          rScale(maxValue * 1.25) * Math.cos(angleSlice * i - Math.PI / 2)
      )
      .attr(
        'y',i) =>
          rScale(maxValue * 1.25) * Math.sin(angleSlice * i - Math.PI / 2)
      )
      .text((d) => {
        return keysToString[d];
      })
      .call(wrap,60);

    // need tempo range 0-200 x
    // need song key to be [0-11]
    // songPositions are the dots on the board
    let songPositions;
    let newData;
   
    function modeChange(){
      let circleColor;
      let textColor;
      //if statements changes mode from minor 0 to major 1
      if (d3.select("#checkBox").property("checked")) {
        newData = data.filter((el,i) => i <= 20 && el.mode === 1)
        circleColor ='#360071'
        textColor = 'black'
      }
      else {
        newData = data.filter((el,i) => i <= 20 && el.mode === 0)
        circleColor = 'beige'
        textColor = 'white'
      };

      songPositions = g
      .selectAll('.songPositions')
      .data(newData,(d) => d) // currently selecting less than 20 songs in DB and only songs in Major Keys
      .join('g')
      .attr('class','.songPositions');
      
      songPositions
        .append('circle')
        .attr('class','radarCircle')
        .attr('r',4)
        .attr('cx',function (d,i) {
          return (
            rScale(d.tempo) *
            Math.cos(angleSlice * apiNotesToWheelNotes[d.key] - Math.PI / 2)
          );
        })
        .attr('cy',i) {
          return (
            rScale(d.tempo) *
            Math.sin(angleSlice * apiNotesToWheelNotes[d.key] - Math.PI / 2)
          );
        })
        .style('fill',circleColor)
        .style('fill-opacity',0.8)

        
      //append text label to dots
      songPositions
        .append('text')
        .attr('class','songName')
        .style('font-size','8px')
        .attr('text-anchor','right')
        .style('fill',textColor)
        .attr('dy','1em')
        .attr('dx','1em')
        .attr('x',i) {
          return (
            rScale(d.tempo) *
            Math.cos(angleSlice * apiNotesToWheelNotes[d.key] - Math.PI / 2)
          );
        })
        .attr('y',i) {
          return (
            rScale(d.tempo) *
            Math.sin(angleSlice * apiNotesToWheelNotes[d.key] - Math.PI / 2)
          );
        })
        .text((d) => `${d.name} - ${d.artists}`)
        .call(wrap,60);
    }
    modeChange()

    function filtergraph(){
      //artist is the variable from gql search
      
      // let songPositions;
      // let newData;
      let circleColor;
      let textColor;
      //if statements changes mode from minor 0 to major 1
      if (d3.select("#checkBox").property("checked")) {
        //add artist category/variable
        newData = data.filter((el) => el.artists == artist)
        circleColor ='#360071'
        textColor = 'black'
      }
      else {
        //add artist category/variable
        newData = data.filter((el) => {
          return el.artists == artist;
        })
        console.log('newData false',data.filter((el) => {
          return el.artists == artist;
        }))
        circleColor = 'beige'
        textColor = 'white'
      };
  
      
  
      songPositions = g
      .selectAll('.songPositions')
      .data(newData,0.8)
  
        
      //append text label to dots
      songPositions
        .append('text')
        .attr('class',60);
    }
    
  };

  



  setActiveDatapoint = (d,node) => {
    d3.select(node).style('fill','yellow');
    this.props.onDatapointClick(d);
  };

  resize = (width,height) => {
    const { svg } = this;
    svg.attr('width',width).attr('height',height);
    svg
      .selectAll('circle')
      .attr('cx',() => 0.3 * width)
      .attr('cy',() => 0.1 * height);
  };
}

function wrap(text,width) {
  text.each(function () {
    var text = d3.select(this),words = text.text().split(/\s+/).reverse(),word,line = [],lineNumber = 0,lineHeight = 1.4,// ems
      x = text.attr('x'),y = text.attr('y'),dy = parseFloat(text.attr('dy')),tspan = text
        .text(null)
        .append('tspan')
        .attr('x',x)
        .attr('y',y)
        .attr('dy',dy + 'em');

    while ((word = words.pop())) {
      line.push(word);
      tspan.text(line.join(' '));
      if (tspan.node().getComputedTextLength() > width) {
        line.pop();
        tspan.text(line.join(' '));
        line = [word];
        tspan = text
          .append('tspan')
          .attr('x',x)
          .attr('y',y)
          .attr('dy',++lineNumber * lineHeight + dy + 'em')
          .text(word);
      }
    }
  });
} //wrap ```


解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)