Add settings page for managing forecasting API key and URL via UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-20 14:37:36 +00:00
parent cae411eae7
commit 1c411e402e
19809 changed files with 1962608 additions and 97 deletions

View file

@ -0,0 +1,73 @@
/**
* A collection of all default zIndex values used by Recharts.
*
* You can reuse these, or you can define your own.
*/
export var DefaultZIndexes = {
/**
* CartesianGrid and PolarGrid
*/
grid: -100,
/**
* Background of Bar and RadialBar.
* This is not visible by default but can be enabled by setting background={true} on Bar or RadialBar.
*/
barBackground: -50,
/*
* other chart elements or custom elements without specific zIndex
* render in here, at zIndex 0
*/
/**
* Area, Pie, Radar, and ReferenceArea
*/
area: 100,
/**
* Cursor is embedded inside Tooltip and controlled by it.
* The Tooltip itself has a separate portal and is not included in the zIndex system;
* Cursor is the decoration inside the chart area. CursorRectangle is a rectangle box.
* It renders below bar so that in a stacked bar chart the cursor rectangle does not hide the other bars.
*/
cursorRectangle: 200,
/**
* Bar and RadialBar
*/
bar: 300,
/**
* Line and ReferenceLine, and ErrorBor
*/
line: 400,
/**
* XAxis and YAxis and PolarAngleAxis and PolarRadiusAxis ticks and lines and children
*/
axis: 500,
/**
* Scatter and ReferenceDot,
* and Dots of Line and Area and Radar if they have dot=true
*/
scatter: 600,
/**
* Hovering over a Bar or RadialBar renders a highlight rectangle
*/
activeBar: 1000,
/**
* Cursor is embedded inside Tooltip and controlled by it.
* The Tooltip itself has a separate portal and is not included in the zIndex system;
* Cursor is the decoration inside the chart area, usually a cross or a box.
* CursorLine is a line cursor rendered in Line, Area, Scatter, Radar charts.
* It renders above the Line and Scatter so that it is always visible.
* It renders below active dot so that the dot is always visible and shows the current point.
* We're also assuming that the active dot is small enough that it does not fully cover the cursor line.
*
* This also applies to the radial cursor in RadialBarChart.
*/
cursorLine: 1100,
/**
* Hovering over a Point in Line, Area, Scatter, Radar renders a highlight dot
*/
activeDot: 1200,
/**
* LabelList and Label, including Axis labels
*/
label: 2000
};

View file

@ -0,0 +1,133 @@
import { useLayoutEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { useAppDispatch, useAppSelector } from '../state/hooks';
import { selectZIndexPortalElement } from './zIndexSelectors';
import { registerZIndexPortal, unregisterZIndexPortal } from '../state/zIndexSlice';
import { useIsInChartContext } from '../context/chartLayoutContext';
import { useIsPanorama } from '../context/PanoramaContext';
/**
* @since 3.4
*/
/**
* A layer that renders its children into a portal corresponding to the given zIndex.
* We can't use regular CSS `z-index` because SVG does not support it.
* So instead, we create separate DOM nodes for each zIndex layer
* and render the children into the corresponding DOM node using React portals.
*
* This component must be used inside a Chart component.
*
* @param zIndex numeric zIndex value, higher values are rendered on top of lower values
* @param children the content to render inside this zIndex layer
*
* @since 3.4
*/
export function ZIndexLayer(_ref) {
var zIndex = _ref.zIndex,
children = _ref.children;
/*
* If we are outside of chart, then we can't rely on the zIndex portal state,
* so we just render normally.
*/
var isInChartContext = useIsInChartContext();
/*
* If zIndex is undefined then we render normally without portals.
* Also, if zIndex is 0, we render normally without portals,
* because 0 is the default layer that does not need a portal.
*/
var shouldRenderInPortal = isInChartContext && zIndex !== undefined && zIndex !== 0;
var isPanorama = useIsPanorama();
/**
* When zIndex changes, the new portal element is not immediately available because
* it requires a full render cycle through AllZIndexPortals ZIndexSvgPortal.
* During this transition we keep rendering into the previous portal element
* to avoid an unmount/remount cycle that would cause children to briefly disappear.
*
* `registeredZIndexesRef` tracks every zIndex we have registered so that
* we can defer unregistration of old values until the new portal is ready.
* `lastPortalElementRef` caches the most recent valid portal DOM node.
*/
var lastPortalElementRef = useRef(undefined);
var registeredZIndexesRef = useRef(new Set());
var dispatch = useAppDispatch();
var portalElement = useAppSelector(state => selectZIndexPortalElement(state, zIndex, isPanorama));
/*
* Lifecycle effect handles both registration and deferred cleanup.
*
* Registration: when zIndex changes we register the new value WITHOUT
* immediately unregistering the old one. This keeps the old <g> element
* alive in the DOM so `lastPortalElementRef` remains a valid render target.
*
* Deferred cleanup: once `portalElement` for the *new* zIndex becomes
* available we unregister every stale zIndex that is no longer needed.
*/
useLayoutEffect(() => {
if (!shouldRenderInPortal) {
// Portal rendering was disabled — clean up any stale registrations
var registered = registeredZIndexesRef.current;
registered.forEach(z => {
dispatch(unregisterZIndexPortal({
zIndex: z
}));
});
registered.clear();
lastPortalElementRef.current = undefined;
return;
}
/*
* Because zIndexes are dynamic (meaning, we're not working with a predefined set of layers,
* but we allow users to define any zIndex at any time), we need to register
* the requested zIndex in the global store. This way, the ZIndexPortals component
* can render the corresponding portals and only the requested ones.
*/
// Register the current zIndex (idempotent — skips if already registered)
if (!registeredZIndexesRef.current.has(zIndex)) {
dispatch(registerZIndexPortal({
zIndex
}));
registeredZIndexesRef.current.add(zIndex);
}
// When the new portal element is ready, retire old zIndex registrations
if (portalElement) {
lastPortalElementRef.current = portalElement;
var _registered = registeredZIndexesRef.current;
_registered.forEach(z => {
if (z !== zIndex) {
dispatch(unregisterZIndexPortal({
zIndex: z
}));
_registered.delete(z);
}
});
}
}, [dispatch, zIndex, shouldRenderInPortal, portalElement]);
// Unmount-only cleanup — unregister everything when the component is removed
useLayoutEffect(() => {
var registered = registeredZIndexesRef.current;
return () => {
registered.forEach(z => {
dispatch(unregisterZIndexPortal({
zIndex: z
}));
});
registered.clear();
};
}, [dispatch]);
if (!shouldRenderInPortal) {
return children;
}
// Prefer the current portal; fall back to the cached one during transitions
var targetElement = portalElement !== null && portalElement !== void 0 ? portalElement : lastPortalElementRef.current;
if (!targetElement) {
// Very first render — no portal has ever been registered yet
return null;
}
return /*#__PURE__*/createPortal(children, targetElement);
}

View file

@ -0,0 +1,52 @@
import * as React from 'react';
import { useLayoutEffect, useRef } from 'react';
import { useAppDispatch, useAppSelector } from '../state/hooks';
import { registerZIndexPortalElement, unregisterZIndexPortalElement } from '../state/zIndexSlice';
import { selectAllRegisteredZIndexes } from './zIndexSelectors';
function ZIndexSvgPortal(_ref) {
var zIndex = _ref.zIndex,
isPanorama = _ref.isPanorama;
var ref = useRef(null);
var dispatch = useAppDispatch();
useLayoutEffect(() => {
if (ref.current) {
dispatch(registerZIndexPortalElement({
zIndex,
element: ref.current,
isPanorama
}));
}
return () => {
dispatch(unregisterZIndexPortalElement({
zIndex,
isPanorama
}));
};
}, [dispatch, zIndex, isPanorama]);
// these g elements should not be tabbable
return /*#__PURE__*/React.createElement("g", {
tabIndex: -1,
ref: ref,
className: "recharts-zIndex-layer_".concat(zIndex)
});
}
export function AllZIndexPortals(_ref2) {
var children = _ref2.children,
isPanorama = _ref2.isPanorama;
var allRegisteredZIndexes = useAppSelector(selectAllRegisteredZIndexes);
if (!allRegisteredZIndexes || allRegisteredZIndexes.length === 0) {
return children;
}
var allNegativeZIndexes = allRegisteredZIndexes.filter(zIndex => zIndex < 0);
// We exclude zero on purpose - that is the default layer, and it doesn't need a portal.
var allPositiveZIndexes = allRegisteredZIndexes.filter(zIndex => zIndex > 0);
return /*#__PURE__*/React.createElement(React.Fragment, null, allNegativeZIndexes.map(zIndex => /*#__PURE__*/React.createElement(ZIndexSvgPortal, {
key: zIndex,
zIndex: zIndex,
isPanorama: isPanorama
})), children, allPositiveZIndexes.map(zIndex => /*#__PURE__*/React.createElement(ZIndexSvgPortal, {
key: zIndex,
zIndex: zIndex,
isPanorama: isPanorama
})));
}

View file

@ -0,0 +1,7 @@
import { isWellBehavedNumber } from '../util/isWellBehavedNumber';
export function getZIndexFromUnknown(input, defaultZIndex) {
if (input && typeof input === 'object' && 'zIndex' in input && typeof input.zIndex === 'number' && isWellBehavedNumber(input.zIndex)) {
return input.zIndex;
}
return defaultZIndex;
}

View file

@ -0,0 +1,32 @@
import { createSelector } from 'reselect';
import { arrayContentsAreEqualCheck } from '../state/selectors/arrayEqualityCheck';
import { DefaultZIndexes } from './DefaultZIndexes';
/**
* Given a zIndex, returns the corresponding portal element reference.
* If no zIndex is provided or if the zIndex is not registered, returns undefined.
*
* It also returns undefined in case the z-index portal has not been rendered yet.
*/
export var selectZIndexPortalElement = createSelector(state => state.zIndex.zIndexMap, (_, zIndex) => zIndex, (_, _zIndex, isPanorama) => isPanorama, (zIndexMap, zIndex, isPanorama) => {
if (zIndex == null) {
return undefined;
}
var entry = zIndexMap[zIndex];
if (entry == null) {
return undefined;
}
if (isPanorama) {
return entry.panoramaElement;
}
return entry.element;
});
export var selectAllRegisteredZIndexes = createSelector(state => state.zIndex.zIndexMap, zIndexMap => {
var allNumbers = Object.keys(zIndexMap).map(zIndexStr => parseInt(zIndexStr, 10)).concat(Object.values(DefaultZIndexes));
var uniqueNumbers = Array.from(new Set(allNumbers));
return uniqueNumbers.sort((a, b) => a - b);
}, {
memoizeOptions: {
resultEqualityCheck: arrayContentsAreEqualCheck
}
});