import { AxisBottom, AxisLeft } from "@visx/axis"; import { Group } from "@visx/group"; import { useParentSize } from "@visx/responsive"; import { scaleLinear, scaleTime } from "@visx/scale"; import { observer } from "mobx-react"; import { FC, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; import ChartableMixin, { ChartItem } from "../../../ModelMixins/ChartableMixin"; import MappableMixin from "../../../ModelMixins/MappableMixin"; import LineChart from "./LineChart"; import Styles from "./chart-preview.scss"; type CatalogItemType = ChartableMixin.Instance; type FeatureInfoPanelChartPropTypes = { item: CatalogItemType; width?: number; height?: number; xAxisLabel?: string; yColumn?: string; margin?: Margin; baseColor?: string; }; interface Margin { top: number; left: number; right: number; bottom: number; } const defaultMargin: Margin = { top: 5, left: 5, right: 5, bottom: 5 }; /** * Chart component for feature info panel popup */ const FeatureInfoPanelChart: FC = observer( (props) => { const [loadingFailed, setLoadingFailed] = useState(false); const { t } = useTranslation(); const parentSize = useParentSize(); const width = props.width || Math.max(parentSize.width, 300) || 0; const height = props.height || Math.max(parentSize.height, 200) || 0; const catalogItem = props.item; // If a yColumn is specified, use it if it is of line type, otherwise use // the first line type chart item. let chartItem = props.yColumn ? catalogItem.chartItems.find((it) => it.id === props.yColumn) : catalogItem.chartItems.find(isLineType); chartItem = chartItem && isLineType(chartItem) ? chartItem : undefined; const notChartable = !ChartableMixin.isMixedInto(catalogItem); const isLoading = !chartItem && MappableMixin.isMixedInto(catalogItem) && catalogItem.isLoadingMapItems; const noData = !chartItem || chartItem.points.length === 0; // Text to show when chart is not ready or available const chartStatus = notChartable ? "chart.noData" : isLoading ? "chart.loading" : loadingFailed ? "chart.noData" : noData ? "chart.noData" : undefined; const canShowChart = chartStatus === undefined; const margin = { ...defaultMargin, ...props.margin }; const baseColor = props.baseColor ?? "#efefef"; useEffect(() => { if (MappableMixin.isMixedInto(catalogItem)) { catalogItem.loadMapItems().then((result) => { setLoadingFailed(result.error !== undefined); result.logError(); }); } else { setLoadingFailed(false); } }, [catalogItem]); return (
{!canShowChart && ( {t(chartStatus)} )} {canShowChart && chartItem && ( )}
); } ); const isLineType = (chartItem: ChartItem) => chartItem.type === "line" || chartItem.type === "lineAndPoint"; interface ChartPropsType { width: number; height: number; margin: Margin; chartItem: ChartItem; baseColor: string; xAxisLabel?: string; } /** * Private Chart component that renders the SVG chart */ const Chart: FC = observer( ({ width, height, margin, chartItem, baseColor, xAxisLabel }) => { const xAxisHeight = 30; const yAxisWidth = 10; const plot = useMemo(() => { return { width: width - margin.left - margin.right, height: height - margin.top - margin.bottom - xAxisHeight }; }, [width, height, margin]); const scales = useMemo(() => { const xScaleParams = { domain: chartItem.domain.x, range: [margin.left + yAxisWidth, plot.width] }; const yScaleParams = { domain: chartItem.domain.y, range: [plot.height, 0] }; return { x: chartItem.xAxis.scale === "linear" ? scaleLinear(xScaleParams) : scaleTime(xScaleParams), y: scaleLinear(yScaleParams) }; }, [ chartItem.domain.x, chartItem.domain.y, chartItem.xAxis.scale, margin.left, plot.height, plot.width ]); const textStyle = { fill: baseColor, fontSize: 10, textAnchor: "middle", fontFamily: "Arial" }; const chartLabel = xAxisLabel ?? defaultChartLabel({ xName: chartItem.xAxis.name, xUnits: chartItem.xAxis.units, yName: chartItem.name, yUnits: chartItem.units }); useEffect(() => { chartItem.points = chartItem.points.sort( (a, b) => scales.x(a.x) - scales.x(b.x) ); }); return ( { // To prevent the first and last values from getting clipped, // we position the first label text to start at the tick position // and the last label text to finish at the tick position. For all // others, middle of the text will coincide with the tick position. const textAnchor = i === 0 ? "start" : i === ticks.length - 1 ? "end" : "middle"; return { ...textStyle, textAnchor }; }} label={chartLabel} labelOffset={3} labelProps={{ fill: baseColor, fontSize: 10, textAnchor: "middle", fontFamily: "Arial" }} /> ({ ...textStyle, textAnchor: "start", dx: "1em", dy: "0" })} /> ); } ); Chart.displayName = "Chart"; export const ChartStatusText = styled.div<{ width: number; height: number }>` display: flex; align-items: center; justify-content: center; width: ${(p) => p.width}px; height: ${(p) => p.height}px; `; const defaultChartLabel = (opts: { xName: string; xUnits?: string; yName: string; yUnits?: string; }) => `${withUnits(opts.yName, opts.yUnits)} x ${withUnits( opts.xName, opts.xUnits )}`; const withUnits = (name: string, units?: string) => units ? `${name} (${units})` : name; export default FeatureInfoPanelChart;