import * as React from 'react'; import './periodic-table.module.less'; import { DISPLAY_MODE, PeriodicElement } from '../periodic-element/periodic-element.component'; import { useDetailedElement } from '../periodic-table-state/table-store'; import { MatElement, TABLE_DICO_V2, TABLE_V2 } from '../periodic-table-data/table-v2'; import { useMediaQuery } from 'react-responsive'; import { useMemo } from 'react'; import { extent, range, max, min } from 'd3-array'; import { scaleLinear, scaleSequential } from 'd3-scale'; import * as d3Scale from 'd3-scale-chromatic'; import { PeriodicTableSpacer } from '../PeriodicTable/PeriodicTableSpacer'; import classNames from 'classnames'; export const DEFAULT_HEATMAP_COLOR = '#EDEEED'; export const COLORSCHEME = { Viridis: d3Scale.interpolateViridis, Turbo: d3Scale.interpolateTurbo, CubeHelix: d3Scale.interpolateCubehelixDefault, Cividis: d3Scale.interpolateCividis, Inferno: d3Scale.interpolateInferno, Blues: d3Scale.interpolateBlues, Oranges: d3Scale.interpolateOranges, Greens: d3Scale.interpolateGreens, Reds: d3Scale.interpolateReds, Purples: d3Scale.interpolatePurples }; Object.freeze(COLORSCHEME); export interface TableProps { className?: string; /** dictionnary of disabled elements */ disabledElement: { [symbol: string]: boolean }; /** dictionnary of enabled elements */ enabledElement: { [symbol: string]: boolean }; /** dictonnary of hidden elements */ hiddenElement: { [symbol: string]: boolean }; /** Callback who gets called once the user clicked an element; the clicked element is passed **/ onElementClicked: (mat: MatElement) => void; onElementMouseOver: (mat: MatElement) => void; onElementMouseLeave?: (mat: MatElement) => void; /** Force the layout of the table **/ forceTableLayout?: TableLayout; /** Colorize the table by using an heatmap * id: symbol of the element * value: value used for the heatmap **/ heatmap?: { [id: string]: number }; /** Color scheme to use. Look at the COLORSCHEME definition for available color scheme * If you pass an unknown color scheme or no scheme, the component will use a linear scale whose range * is defined by the heatmapMax and heatmapMin value **/ colorScheme?: keyof typeof COLORSCHEME; /** Color used for the highest value */ heatmapMax?: string; /** Color used for the lowest value*/ heatmapMin?: string; showSwitcher?: boolean; plugin?: JSX.Element; children?: any; selectorWidget?: any; /** toggle disabling all table components */ disabled?: boolean; } export enum TableLayout { SPACED = 'spaced', COMPACT = 'compact', MINI = 'small', MAP = 'map' } const N_LEGEND_ITEMS = 10; function computeHeatmap( h: { [symbol: string]: number }, max: string, min: string, scheme: keyof typeof COLORSCHEME ) { if (!h) return { linearScale: null, legendScale: null }; const heatmapExtent = extent(Object.values(h)); const legendPosition = scaleLinear().domain(heatmapExtent).range([0, 100]); if (COLORSCHEME[scheme]) { return { linearScale: scaleSequential(COLORSCHEME[scheme]).domain(heatmapExtent), legendScale: scaleSequential(COLORSCHEME[scheme]).domain([0, N_LEGEND_ITEMS]), legendPosition }; } const linearScale = scaleLinear().range([min, max]); linearScale.domain(heatmapExtent); const legendScale = scaleLinear().range([min, max]).domain([0, N_LEGEND_ITEMS]); return { linearScale, legendScale, legendPosition }; } export function Table({ className, disabledElement, enabledElement, hiddenElement, onElementClicked, onElementMouseOver, onElementMouseLeave = () => {}, forceTableLayout, heatmap, heatmapMax, heatmapMin, colorScheme, showSwitcher, selectorWidget, plugin, disabled }: TableProps) { const [isShown, setIsShown] = React.useState(true); const [legendPosition, setLegendPosition] = React.useState(-1); const isDesktop = useMediaQuery({ minWidth: 1436 }); const isTablet = useMediaQuery({ minWidth: 900, maxWidth: 1436 }); const isMobile = useMediaQuery({ maxWidth: 900 }); // DO NOT REFACTORING USING || IT WILL CRASH THE APP, BECAUSE A DIFFERENT NUMBER OF HOOKS WILL BE // CALLED const isDesktopH = useMediaQuery({ minHeight: 670 }); const isTabletH = useMediaQuery({ maxHeight: 670, minHeight: 400 }); const isMobileH = useMediaQuery({ maxHeight: 400 }); // we consider that either those properties are all defined, or not const { linearScale: heatmapscale, legendScale, legendPosition: legendPositionScale } = useMemo( () => computeHeatmap(heatmap!, heatmapMax!, heatmapMin!, colorScheme!), [heatmapMax, heatmapMin, heatmap, colorScheme] ); // TODO(chab) allow people to pass the number of subdivisions OR to have a continuous legend const legendItems = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; const onHover = (element: MatElement) => { if (!hasHeatmap(heatmap)) { onElementMouseOver(element); return; } const value = heatmap![element.symbol]; if (!value) { setLegendPosition(-1); } else { const legendPosition = legendPositionScale(value); setLegendPosition(legendPosition); } onElementMouseOver(element); }; return (