import React, { useMemo, useRef, createContext, useContext, useState, useEffect, } from 'react' import { useAtom } from 'jotai' import { usePageVisibility } from 'react-page-visibility' import type { Aes, DataValue, GGProps } from './types' import { defineGroupAccessor } from '../util/defineGroupAccessor' import { flattenChildren } from '../util/flattenChildren' import { type IScale, autoScale } from '../util/autoScale' import { XAxis, YAxis } from './axes' import { themeState, labelsState, xScaleState, yScaleState, fillScaleState, strokeScaleState, strokeDasharrayState, } from '../atoms' export interface ContextProps { ggState: { id?: string width: number height: number margin: { top: number right: number bottom: number left: number } data: Datum[] copiedData: Datum[] aes: Aes copiedScales: IScale scales: IScale } updateData: (newData: Datum[]) => void } const GGglobalCtx = createContext | undefined>(undefined) export const GGBase = ({ data, aes, width = 500, height = 450, margin: suppliedMargin, id, children, }: GGProps) => { const [labels] = useAtom(labelsState) const [{ font, headerColor, axis, axisX, axisY, animationDuration }] = useAtom(themeState) const [xScale] = useAtom(xScaleState) const [yScale] = useAtom(yScaleState) const [fillScale] = useAtom(fillScaleState) const [strokeScale] = useAtom(strokeScaleState) const [strokeDasharrayScale] = useAtom(strokeDasharrayState) const isVisible = usePageVisibility() const [ggData, setGGData] = useState(data) const margin = { top: 10, right: 20, bottom: 40, left: 30, ...suppliedMargin, } const ggWidth = Math.min(window.innerWidth - 15, width) const copiedData = data const geoms: React.ReactNode[] = [] const otherChildren: React.ReactNode[] = [] let shouldExcludeMissingXYFromDomains = false flattenChildren(children).forEach((child) => { if (React.isValidElement(child)) { const thisChild: any = child.type if (thisChild?.displayName?.includes('Geom')) { geoms.push(child) } else { otherChildren.push(child) } } }) const geomPositions: (string | undefined)[] = [] const geomZeroXBaseLines: (boolean | undefined)[] = [] const geomZeroYBaseLines: (boolean | undefined)[] = [] const geomAesXs = [] const geomAesYs: DataValue[] = [] const geomAesY0s: DataValue[] = [] const geomAesY1s: DataValue[] = [] const geomGroupAccessors: DataValue[] = [] const geomAesStrokes: DataValue[] = [] const geomAesStrokeDasharrays: DataValue[] = [] const geomAesFills: DataValue[] = [] geoms.forEach((g: any) => { const geomProps = g.props const geomGroupAccessor = defineGroupAccessor(geomProps.aes, true) if (geomGroupAccessor) geomGroupAccessors.push(geomGroupAccessor) if (geomProps.aes?.x) geomAesXs.push(geomProps.aes.x) if (geomProps.aes?.y) geomAesYs.push(geomProps.aes.y) if (geomProps.aes?.y0) geomAesY0s.push(geomProps.aes.y0) if (geomProps.aes?.y1) geomAesY1s.push(geomProps.aes.y1) if (geomProps.aes?.stroke) geomAesStrokes.push(geomProps.aes.stroke) if (geomProps.aes?.strokeDasharray) geomAesStrokeDasharrays.push(geomProps.aes.strokeDasharray) if (geomProps.aes?.fill) geomAesFills.push(geomProps.aes.fill) const isBar = g.type.displayName.includes('Bar') if (isBar) { geomZeroXBaseLines.push(!geomProps.freeBaseLine) } if (!isBar) { geomPositions.push(geomProps.position) } if (g.type.displayName.includes('Col')) { geomZeroYBaseLines.push(!geomProps.freeBaseLine) } if (g.type.displayName.includes('Tile')) { shouldExcludeMissingXYFromDomains = true } }) const areaGeom: any = geoms.find((g: any) => g.type.displayName.includes('Area'), ) const y0Aes = areaGeom?.props?.aes?.y0 const y1Aes = areaGeom?.props?.aes?.y1 const hasZeroXBaseLine = geomZeroXBaseLines.some((v) => v) const hasZeroYBaseLine = geomZeroYBaseLines.some((v) => v) const ggState = useMemo( () => ({ id, copiedData, data: ggData, aes, width: ggWidth, height, margin, copiedScales: autoScale({ scalesState: { x: xScale, y: yScale, geomAesYs, y0Aes, y1Aes, hasZeroXBaseLine, hasZeroYBaseLine, geomGroupAccessors, geomAesStrokes, geomAesStrokeDasharrays, geomAesFills, fill: fillScale, stroke: strokeScale, strokeDasharray: strokeDasharrayScale, }, data, copiedData, aes, width: ggWidth, height, margin, shouldExcludeMissingXYFromDomains, }), scales: autoScale({ scalesState: { x: xScale, y: yScale, geomAesYs, y0Aes, y1Aes, hasZeroXBaseLine, hasZeroYBaseLine, geomGroupAccessors, geomAesStrokes, geomAesStrokeDasharrays, geomAesFills, fill: fillScale, stroke: strokeScale, strokeDasharray: strokeDasharrayScale, }, data: ggData, copiedData, aes, width: ggWidth, height, margin, shouldExcludeMissingXYFromDomains, }), }), [ id, data, ggData, copiedData, aes, ggWidth, height, margin, xScale, yScale, fillScale, strokeScale, strokeDasharrayScale, hasZeroXBaseLine, hasZeroYBaseLine, ], ) const updateData = (newData: typeof data) => { setGGData(newData) } useEffect(() => { setGGData(data) }, [data]) const ggRef = useRef(null) return ggState ? (
{labels?.header && (
{labels.header}
)} {axis && axisY && labels?.y && (
{labels?.y}
)} {isVisible && ( <> {axis && axisX && ( 0 } /> )} {axis && axisY && ( 0 } /> )} {geoms} )} {/* zoom out portal */}
{/* tooltip portals */}
{/* other types of children */}
{otherChildren}
) : null } export const useGG = () => useContext(GGglobalCtx as React.Context>)