通过 Nivo 条形图过滤数据

Filter data going through Nivo Bar Chart

使用 react-table,我可以很好地过滤数据,但希望它更动态

现在我正在努力完成的是过滤图表中的数据,使其更有意义,部分方法是将单一品牌的产品集中在一个条形图中,同时删除过多的品牌信息更难理解。

这是我正在使用的 Table 组件

import React from 'react'
import {
    useTable,
    usePagination,
    useSortBy,
    useFilters,
    useGroupBy,
    useExpanded,
    useRowSelect,
  } from 'react-table'
  import matchSorter from 'match-sorter'
  
  // import makeData from '../example/makedata.js'
  
  
  // Create an editable cell renderer
  const EditableCell = ({
    value: initialValue,
    row: { index },
    column: { id },
    updateMyData, // This is a custom function that we supplied to our table instance
    editable,
  }) => {
    // We need to keep and update the state of the cell normally
    const [value, setValue] = React.useState(initialValue)
  
    const onChange = e => {
      setValue(e.target.value)
    }
  
    // We'll only update the external data when the input is blurred
    const onBlur = () => {
      updateMyData(index, id, value)
    }
  
    // If the initialValue is changed externall, sync it up with our state
    React.useEffect(() => {
      setValue(initialValue)
    }, [initialValue])
  
    if (!editable) {
      return `${initialValue}`
    }
  
    return <input value={value} onChange={onChange} onBlur={onBlur} />
  }
  
  // Define a default UI for filtering
  function DefaultColumnFilter({
    column: { filterValue, preFilteredRows, setFilter },
  }) {
    const count = preFilteredRows.length
  
    return (
      <input
        value={filterValue || ''}
        onChange={e => {
          setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely
        }}
        placeholder={`Search ${count} records...`}
      />
    )
  }
  
  // This is a custom filter UI for selecting
  // a unique option from a list
  function SelectColumnFilter({
    column: { filterValue, setFilter, preFilteredRows, id },
  }) {
    // Calculate the options for filtering
    // using the preFilteredRows
    const options = React.useMemo(() => {
      const options = new Set()
      preFilteredRows.forEach(row => {
        options.add(row.values[id])
      })
      return [...options.values()]
    }, [id, preFilteredRows])
  
    // Render a multi-select box
    return (
      <select
        value={filterValue}
        onChange={e => {
          setFilter(e.target.value || undefined)
        }}
      >
        <option value="">All</option>
        {options.map((option, i) => (
          <option key={i} value={option}>
            {option}
          </option>
        ))}
      </select>
    )
  }
  
  // This is a custom filter UI that uses a
  // slider to set the filter value between a column's
  // min and max values
  function SliderColumnFilter({
    column: { filterValue, setFilter, preFilteredRows, id },
  }) {
    // Calculate the min and max
    // using the preFilteredRows
  
    const [min, max] = React.useMemo(() => {
      let min = preFilteredRows.length ? preFilteredRows[0].values[id] : 0
      let max = preFilteredRows.length ? preFilteredRows[0].values[id] : 0
      preFilteredRows.forEach(row => {
        min = Math.min(row.values[id], min)
        max = Math.max(row.values[id], max)
      })
      return [min, max]
    }, [id, preFilteredRows])
  
    return (
      <>
        <input
          type="range"
          min={min}
          max={max}
          value={filterValue || min}
          onChange={e => {
            setFilter(parseInt(e.target.value, 10))
          }}
        />
        <button onClick={() => setFilter(undefined)}>Off</button>
      </>
    )
  }
  
  // This is a custom UI for our 'between' or number range
  // filter. It uses two number boxes and filters rows to
  // ones that have values between the two
  function NumberRangeColumnFilter({
    column: { filterValue = [], preFilteredRows, setFilter, id },
  }) {
    const [min, max] = React.useMemo(() => {
      let min = preFilteredRows.length ? preFilteredRows[0].values[id] : 0
      let max = preFilteredRows.length ? preFilteredRows[0].values[id] : 0
      preFilteredRows.forEach(row => {
        min = Math.min(row.values[id], min)
        max = Math.max(row.values[id], max)
      })
      return [min, max]
    }, [id, preFilteredRows])
  
    return (
      <div
        style={{
          display: 'flex',
        }}
      >
        <input
          value={filterValue[0] || ''}
          type="number"
          onChange={e => {
            const val = e.target.value
            setFilter((old = []) => [val ? parseInt(val, 10) : undefined, old[1]])
          }}
          placeholder={`Min (${min})`}
          style={{
            width: '70px',
            marginRight: '0.5rem',
          }}
        />
        to
        <input
          value={filterValue[1] || ''}
          type="number"
          onChange={e => {
            const val = e.target.value
            setFilter((old = []) => [old[0], val ? parseInt(val, 10) : undefined])
          }}
          placeholder={`Max (${max})`}
          style={{
            width: '70px',
            marginLeft: '0.5rem',
          }}
        />
      </div>
    )
  }
  
  function fuzzyTextFilterFn(rows, id, filterValue) {
    return matchSorter(rows, filterValue, { keys: [row => row.values[id]] })
  }
  
  // Let the table remove the filter if the string is empty
  fuzzyTextFilterFn.autoRemove = val => !val
  
  // Be sure to pass our updateMyData and the skipReset option
  function Table({ columns, data, updateMyData, skipReset }) {
    const td_header = "px-4 py-2"
  const td_style = "border px-4 py-2"
    const filterTypes = React.useMemo(
      () => ({
        // Add a new fuzzyTextFilterFn filter type.
        fuzzyText: fuzzyTextFilterFn,
        // Or, override the default text filter to use
        // "startWith"
        text: (rows, id, filterValue) => {
          return rows.filter(row => {
            const rowValue = row.values[id]
            return rowValue !== undefined
              ? String(rowValue)
                  .toLowerCase()
                  .startsWith(String(filterValue).toLowerCase())
              : true
          })
        },
      }),
      []
    )
  
    const defaultColumn = React.useMemo(
      () => ({
        // Let's set up our default Filter UI
        Filter: DefaultColumnFilter,
        // And also our default editable cell
        Cell: EditableCell,
      }),
      []
    )
  
    // Use the state and functions returned from useTable to build your UI
    const {
      getTableProps,
      getTableBodyProps,
      headerGroups,
      prepareRow,
      page, // Instead of using 'rows', we'll use page,
      // which has only the rows for the active page
  
      // The rest of these things are super handy, too ;)
      canPreviousPage,
      canNextPage,
      pageOptions,
      pageCount,
      gotoPage,
      nextPage,
      previousPage,
      setPageSize,
      state: {
        pageIndex,
        pageSize,
        sortBy,
        groupBy,
        expanded,
        filters,
        selectedRowIds,
      },
    } = useTable(
      {
        columns,
        data,
        defaultColumn,
        filterTypes,
        // updateMyData isn't part of the API, but
        // anything we put into these options will
        // automatically be available on the instance.
        // That way we can call this function from our
        // cell renderer!
        updateMyData,
        // We also need to pass this so the page doesn't change
        // when we edit the data.
        autoResetPage: !skipReset,
        autoResetSelectedRows: !skipReset,
        disableMultiSort: true,
      },
      useFilters,
      useGroupBy,
      useSortBy,
      useExpanded,
      usePagination,
      useRowSelect,
      // Here we will use a plugin to add our selection column
      hooks => {
        hooks.visibleColumns.push(columns => {
          return [
            {
              id: 'selection',
              // Make this column a groupByBoundary. This ensures that groupBy columns
              // are placed after it
              groupByBoundary: true,
              // The header can use the table's getToggleAllRowsSelectedProps method
              // to render a checkbox
              Header: ({ getToggleAllRowsSelectedProps }) => (
                <div>
                  <IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} />
                </div>
              ),
              // The cell can use the individual row's getToggleRowSelectedProps method
              // to the render a checkbox
              Cell: ({ row }) => (
                <div>
                  <IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
                </div>
              ),
            },
            ...columns,
          ]
        })
      }
    )
  
    // Render the UI for your table
    return (
      <>
        <table className="w-full text-md bg-white shadow-md rounded mb-4" {...getTableProps()}>
          <thead>
            {headerGroups.map(headerGroup => (
              <tr {...headerGroup.getHeaderGroupProps()}>
                {headerGroup.headers.map(column => (
                  <th className={td_header} {...column.getHeaderProps()}>
                    <div>
                      {column.canGroupBy ? (
                        // If the column can be grouped, let's add a toggle
                        <span {...column.getGroupByToggleProps()}>
                          {column.isGrouped ? 'Click to Un-Group  Click to Sort!' : ' Click to Group  Click to Sort!'}
                        </span>
                      ) : null}
                      <span {...column.getSortByToggleProps()}>
                        {column.render('Header')}
                        {/* Add a sort direction indicator */}
                        {column.isSorted
                          ? column.isSortedDesc
                            ? ' '
                            : ' '
                          : ''}
                      </span>
                    </div>
                    {/* Render the columns filter UI */}
                    <div>{column.canFilter ? column.render('Filter') : null}</div>
                  </th>
                ))}
              </tr>
            ))}
          </thead>
          <tbody {...getTableBodyProps()}>
            {page.map(row => {
              prepareRow(row)
              return (
                <tr {...row.getRowProps()}>
                  {row.cells.map(cell => {
                    return (
                      <td className = {td_style} {...cell.getCellProps()}>
                        {cell.isGrouped ? (
                          // If it's a grouped cell, add an expander and row count
                          <>
                            <span {...row.getToggleRowExpandedProps()}>
                              {row.isExpanded ? '' : ''}
                            </span>{' '}
                            {cell.render('Cell', { editable: false })} (
                            {row.subRows.length})
                          </>
                        ) : cell.isAggregated ? (
                          // If the cell is aggregated, use the Aggregated
                          // renderer for cell
                          cell.render('Aggregated')
                        ) : cell.isPlaceholder ? null : ( // For cells with repeated values, render null
                          // Otherwise, just render the regular cell
                          cell.render('Cell', { editable: true })
                        )}
                      </td>
                    )
                  })}
                </tr>
              )
            })}
          </tbody>
        </table>
        {/*
          Pagination can be built however you'd like.
          This is just a very basic UI implementation:
        */}
        <div className="pagination">
          <button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
            {'<<'}
          </button>{' '}
          <button onClick={() => previousPage()} disabled={!canPreviousPage}>
            {'<'}
          </button>{' '}
          <button onClick={() => nextPage()} disabled={!canNextPage}>
            {'>'}
          </button>{' '}
          <button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
            {'>>'}
          </button>{' '}
          <span>
            Page{' '}
            <strong>
              {pageIndex + 1} of {pageOptions.length}
            </strong>{' '}
          </span>
          <span>
            | Go to page:{' '}
            <input
              type="number"
              defaultValue={pageIndex + 1}
              onChange={e => {
                const page = e.target.value ? Number(e.target.value) - 1 : 0
                gotoPage(page)
              }}
              style={{ width: '100px' }}
            />
          </span>{' '}
          <select
            value={pageSize}
            onChange={e => {
              setPageSize(Number(e.target.value))
            }}
          >
            {[10, 20, 30, 40, 50].map(pageSize => (
              <option key={pageSize} value={pageSize}>
                Show {pageSize}
              </option>
            ))}
          </select>
        </div>
        <pre>
          {/* <code>
            {JSON.stringify(
              {
                pageIndex,
                pageSize,
                pageCount,
                canNextPage,
                canPreviousPage,
                sortBy,
                groupBy,
                expanded: expanded,
                filters,
                selectedRowIds: selectedRowIds,
              },
              null,
              2
            )}
          </code> */}
        </pre>
      </>
    )
  }
  
  // Define a custom filter filter function!
  function filterGreaterThan(rows, id, filterValue) {
    return rows.filter(row => {
      const rowValue = row.values[id]
      return rowValue >= filterValue
    })
  }
  
  // This is an autoRemove method on the filter function that
  // when given the new filter value and returns true, the filter
  // will be automatically removed. Normally this is just an undefined
  // check, but here, we want to remove the filter if it's not a number
  filterGreaterThan.autoRemove = val => typeof val !== 'number'
  
  // This is a custom aggregator that
  // takes in an array of leaf values and
  // returns the rounded median
  function roundedMedian(leafValues) {
    let min = leafValues[0] || 0
    let max = leafValues[0] || 0
  
    leafValues.forEach(value => {
      min = Math.min(min, value)
      max = Math.max(max, value)
    })
  
    return Math.round((min + max) / 2)
  }
  
  const IndeterminateCheckbox = React.forwardRef(
    ({ indeterminate, ...rest }, ref) => {
      const defaultRef = React.useRef()
      const resolvedRef = ref || defaultRef
  
      React.useEffect(() => {
        resolvedRef.current.indeterminate = indeterminate
      }, [resolvedRef, indeterminate])
  
      return (
        <>
          <input type="checkbox" ref={resolvedRef} {...rest} />
        </>
      )
    }
  )
  
 
  
  export default Table

我希望得到一些帮助,将 react-table 过滤后的数据动态更新或按按钮更新到上面的图形组件中,并过滤初始调用以使其更有用。

更新

下面的精彩代码,我现在收到这个错误:

Uncaught TypeError: Cannot read property 'Price' of undefined
    at stackValue (stack.js:7)
    at stack (stack.js:26)
    at generateVerticalStackedBars (nivo-bar.esm.js:212)
    at generateStackedBars (nivo-bar.esm.js:343)
    at Bar (nivo-bar.esm.js:809)
    at renderWithHooks (react-dom.development.js:14803)
    at updateFunctionComponent (react-dom.development.js:17034)
    at beginWork (react-dom.development.js:18610)
    at HTMLUnknownElement.callCallback (react-dom.development.js:188)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:237)
    at invokeGuardedCallback (react-dom.development.js:292)
    at beginWork (react-dom.development.js:23203)
    at performUnitOfWork (react-dom.development.js:22154)
    at workLoopSync (react-dom.development.js:22130)
    at performSyncWorkOnRoot (react-dom.development.js:21756)
    at react-dom.development.js:11089
    at unstable_runWithPriority (scheduler.development.js:653)
    at runWithPriority (react-dom.development.js:11039)
    at flushSyncCallbackQueueImpl (react-dom.development.js:11084)
    at flushSyncCallbackQueue (react-dom.development.js:11072)
    at discreteUpdates (react-dom.development.js:21893)
    at discreteUpdates (react-dom.development.js:806)
    at dispatchDiscreteEvent (react-dom.development.js:4168)
index.js:1 The above error occurred in the <Bar> component:
    in Bar (created by pure(Bar))
    in pure(Bar) (created by withPropsOnChange(pure(Bar)))
    in withPropsOnChange(pure(Bar)) (created by withPropsOnChange(withPropsOnChange(pure(Bar))))
    in withPropsOnChange(withPropsOnChange(pure(Bar))) (created by withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar)))))
    in withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar)))) (created by withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar))))))
    in withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar))))) (created by withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar)))))))
    in withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar)))))) (created by withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar))))))))
    in withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar))))))) (created by withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar)))))))))
    in withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar)))))))) (created by withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar))))))))))
    in withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar))))))))) (created by defaultProps(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar)))))))))))
    in defaultProps(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar)))))))))) (created by withPropsOnChange(defaultProps(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar))))))))))))
    in withPropsOnChange(defaultProps(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar))))))))))) (created by defaultProps(withPropsOnChange(defaultProps(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar)))))))))))))
    in defaultProps(withPropsOnChange(defaultProps(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar)))))))))))) (created by withPropsOnChange(defaultProps(withPropsOnChange(defaultProps(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar))))))))))))))
    in withPropsOnChange(defaultProps(withPropsOnChange(defaultProps(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(withPropsOnChange(pure(Bar))))))))))))) (created by Bar)
    in Bar (at Chart.js:6)
    in div (at Chart.js:5)
    in Chart (at smartSpy.tsx:276)
    in div (at smartSpy.tsx:275)
    in div (at smartSpy.tsx:274)
    in div (at smartSpy.tsx:273)
    in div (at smartSpy.tsx:222)
    in div (at LayoutExampleEcommerce.js:26)
    in div (at LayoutExampleEcommerce.js:21)
    in div (at LayoutExampleEcommerce.js:14)
    in Layout (at smartSpy.tsx:220)
    in SmartSpy (at _app.js:8)
    in MyApp
    in Container (created by AppContainer)
    in AppContainer

React will try to recreate this component tree from scratch using the error boundary you provided, MyApp.
console.<computed> @ index.js:1
_app.js:7 Uncaught TypeError: Cannot read property 'Price' of undefined
    at stackValue (stack.js:7)
    at stack (stack.js:26)
    at generateVerticalStackedBars (nivo-bar.esm.js:212)
    at generateStackedBars (nivo-bar.esm.js:343)
    at Bar (nivo-bar.esm.js:809)
    at renderWithHooks (react-dom.development.js:14803)
    at updateFunctionComponent (react-dom.development.js:17034)
    at beginWork (react-dom.development.js:18610)
    at HTMLUnknownElement.callCallback (react-dom.development.js:188)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:237)
    at invokeGuardedCallback (react-dom.development.js:292)
    at beginWork (react-dom.development.js:23203)
    at performUnitOfWork (react-dom.development.js:22154)
    at workLoopSync (react-dom.development.js:22130)
    at performSyncWorkOnRoot (react-dom.development.js:21756)
    at react-dom.development.js:11089
    at unstable_runWithPriority (scheduler.development.js:653)
    at runWithPriority (react-dom.development.js:11039)
    at flushSyncCallbackQueueImpl (react-dom.development.js:11084)
    at flushSyncCallbackQueue (react-dom.development.js:11072)
    at discreteUpdates (react-dom.development.js:21893)
    at discreteUpdates (react-dom.development.js:806)
    at dispatchDiscreteEvent (react-dom.development.js:4168)

当数据被 react-table 函数分组时会发生这种情况(通过对单击的列进行分组来完全更改行的值)。

再次感谢

并非所有链接在一起,但应该使解决此问题的方法变得容易理解。

非常感谢!

你可以做的是在 parent 的帮助下在 Table 和 Chart 之间进行通信。使用 React table,您可以从 rows 获取过滤后的数据。您可以使用它并将该信息传递给 parent,后者又会将数据传递给 Chart

    import OriginalData from 'path/to/data';

    const App = () => {
       const [data, setData] = useState(OriginalData);
       const updateData = useCallback((data) => {
           setData(data);
       }, []);
    
       return (
           <>
              <Chart data={data} />
              <Table updateData={updateData} data={originalData}/>
           </>
       )
    }

现在 Table

const Table = ({data, updateData}) => {
    // Use the state and functions returned from useTable to build your UI
    ...
    const {
      getTableProps,
      getTableBodyProps,
      headerGroups,
      prepareRow,
      rows,
      page, // Instead of using 'rows', we'll use page,
      // which has only the rows for the active page

      // The rest of these things are super handy, too ;)
      canPreviousPage,
      canNextPage,
      pageOptions,
      pageCount,
      gotoPage,
      nextPage,
      previousPage,
      setPageSize,
      state: {
        pageIndex,
        pageSize,
        sortBy,
        groupBy,
        expanded,
        filters,
        selectedRowIds,
      },
    } = useTable(
      {
        columns,
        data,
        defaultColumn,
        filterTypes,
        // updateMyData isn't part of the API, but
        // anything we put into these options will
        // automatically be available on the instance.
        // That way we can call this function from our
        // cell renderer!
        updateMyData,
        // We also need to pass this so the page doesn't change
        // when we edit the data.
        autoResetPage: !skipReset,
        autoResetSelectedRows: !skipReset,
        disableMultiSort: true,
      },
      useFilters,
      useGroupBy,
      useSortBy,
      useExpanded,
      usePagination,
      useRowSelect,
      // Here we will use a plugin to add our selection column
      hooks => {
        hooks.visibleColumns.push(columns => {
          return [
            {
              id: 'selection',
              // Make this column a groupByBoundary. This ensures that groupBy columns
              // are placed after it
              groupByBoundary: true,
              // The header can use the table's getToggleAllRowsSelectedProps method
              // to render a checkbox
              Header: ({ getToggleAllRowsSelectedProps }) => (
                <div>
                  <IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} />
                </div>
              ),
              // The cell can use the individual row's getToggleRowSelectedProps method
              // to the render a checkbox
              Cell: ({ row }) => (
                <div>
                  <IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
                </div>
              ),
            },
            ...columns,
          ]
        })
      }
    )
 
    useEffect(() => {
        const newData = rows.map(r => r.original);
        updateData(newData); // update to parent based on change in row
    }, [rows])

    ...
}