From 4e05b3292a2956537bfbfdb3f21534da041b5596 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 4 Oct 2023 15:50:10 +0200 Subject: [PATCH] refactor: Share some logic between AxisHeightLinear and AxisHeightLinearDual --- app/charts/combo/axis-height-linear-dual.tsx | 90 +++------- app/charts/shared/axis-height-linear.tsx | 168 +++++++++++-------- 2 files changed, 122 insertions(+), 136 deletions(-) diff --git a/app/charts/combo/axis-height-linear-dual.tsx b/app/charts/combo/axis-height-linear-dual.tsx index 70f42891f..68ef19225 100644 --- a/app/charts/combo/axis-height-linear-dual.tsx +++ b/app/charts/combo/axis-height-linear-dual.tsx @@ -1,90 +1,45 @@ -import { axisLeft, axisRight } from "d3"; -import { useEffect, useRef } from "react"; +import { alpha } from "@mui/material"; +import React from "react"; import { ComboLineColumnState } from "@/charts/combo/combo-line-column-state"; import { ComboLineDualState } from "@/charts/combo/combo-line-dual-state"; -import { TICK_PADDING } from "@/charts/shared/axis-height-linear"; -import { useChartState } from "@/charts/shared/chart-state"; import { - maybeTransition, - renderContainer, -} from "@/charts/shared/rendering-utils"; -import { getTickNumber } from "@/charts/shared/ticks"; + TICK_PADDING, + useRenderAxisHeightLinear, +} from "@/charts/shared/axis-height-linear"; +import { useChartState } from "@/charts/shared/chart-state"; import { useChartTheme } from "@/charts/shared/use-chart-theme"; import { OpenMetadataPanelWrapper } from "@/components/metadata-panel"; -import { useFormatNumber } from "@/formatters"; -import { useTransitionStore } from "@/stores/transition"; import { getTextWidth } from "@/utils/get-text-width"; type AxisHeightLinearDualProps = { - orientation: "left" | "right"; + orientation?: "left" | "right"; }; export const AxisHeightLinearDual = (props: AxisHeightLinearDualProps) => { - const { orientation } = props; + const { orientation = "left" } = props; const leftAligned = orientation === "left"; - const { labelFontSize, axisLabelFontSize, fontFamily } = useChartTheme(); - const ref = useRef(null); - const enableTransition = useTransitionStore((state) => state.enable); - const transitionDuration = useTransitionStore((state) => state.duration); - const formatNumber = useFormatNumber({ decimals: "auto" }); + const { axisLabelFontSize } = useChartTheme(); + const [ref, setRef] = React.useState(null); const { y, yOrientationScales, colors, bounds, maxRightTickWidth } = useChartState() as ComboLineDualState | ComboLineColumnState; + const yScale = yOrientationScales[orientation]; const { margins } = bounds; - const ticks = getTickNumber(bounds.chartHeight); const axisTitle = y[orientation].label; const axisTitleWidth = getTextWidth(axisTitle, { fontSize: axisLabelFontSize }) + TICK_PADDING; const color = colors(axisTitle); - useEffect(() => { - if (ref.current) { - const makeAxis = (leftAligned ? axisLeft : axisRight)( - yOrientationScales[orientation] - ) - .ticks(ticks) - .tickSizeInner(leftAligned ? -bounds.chartWidth : bounds.chartWidth) - .tickFormat(formatNumber) - .tickPadding(TICK_PADDING); - const g = renderContainer(ref.current, { - id: `axis-height-linear-${orientation}`, - transform: `translate(${margins.left} ${margins.top})`, - transition: { enable: enableTransition, duration: transitionDuration }, - render: (g) => g.call(makeAxis), - renderUpdate: (g, opts) => - maybeTransition(g, { - transition: opts.transition, - s: (g) => g.call(makeAxis), - }), - }); - - g.select(".domain").remove(); - g.selectAll(".tick line") - .attr("stroke", color) - .attr("stroke-width", 1) - .attr("stroke-opacity", 0.1); - g.selectAll(".tick text") - .attr("dy", 3) - .attr("fill", color) - .attr("font-family", fontFamily) - .style("font-size", labelFontSize) - .attr("text-anchor", leftAligned ? "end" : "start"); - } - }, [ - bounds.chartWidth, - enableTransition, - fontFamily, - labelFontSize, - margins.left, - margins.top, - ticks, - transitionDuration, - orientation, - yOrientationScales, - color, - formatNumber, - leftAligned, - ]); + useRenderAxisHeightLinear(ref, { + id: `axis-height-linear-${orientation}`, + orientation: orientation, + scale: yScale, + width: bounds.chartWidth, + height: bounds.chartHeight, + margins: bounds.margins, + lineColor: alpha(color, 0.1), + textColor: color, + }); return ( <> @@ -107,8 +62,7 @@ export const AxisHeightLinearDual = (props: AxisHeightLinearDualProps) => { {axisTitle} - - + setRef(newRef)} /> ); }; diff --git a/app/charts/shared/axis-height-linear.tsx b/app/charts/shared/axis-height-linear.tsx index 7cab99de6..bfccc64c7 100644 --- a/app/charts/shared/axis-height-linear.tsx +++ b/app/charts/shared/axis-height-linear.tsx @@ -1,4 +1,4 @@ -import { axisLeft, NumberValue } from "d3"; +import { axisLeft, axisRight, NumberValue, ScaleLinear } from "d3"; import React, { useEffect, useRef } from "react"; import type { AreasState } from "@/charts/area/areas-state"; @@ -25,19 +25,8 @@ import { getTextWidth } from "@/utils/get-text-width"; export const TICK_PADDING = 6; export const AxisHeightLinear = () => { - const { - labelColor, - labelFontSize, - axisLabelFontSize, - gridColor, - fontFamily, - } = useChartTheme(); - const ref = useRef(null); - const enableTransition = useTransitionStore((state) => state.enable); - const transitionDuration = useTransitionStore((state) => state.duration); - const formatNumber = useFormatNumber({ decimals: "auto" }); - const calculationType = useInteractiveFilters((d) => d.calculation.type); - const normalized = calculationType === "percent"; + const { gridColor, labelColor, axisLabelFontSize } = useChartTheme(); + const [ref, setRef] = React.useState(null); const state = useChartState() as | AreasState | ColumnsState @@ -46,64 +35,21 @@ export const AxisHeightLinear = () => { | LinesState | ScatterplotState | ComboLineSingleState; - - const { margins } = state.bounds; - const ticks = getTickNumber(state.bounds.chartHeight); - const tickFormat = React.useCallback( - (d: NumberValue) => { - return normalized ? `${formatNumber(d)}%` : formatNumber(d); - }, - [formatNumber, normalized] - ); const axisTitleWidth = getTextWidth(state.yAxisLabel, { fontSize: axisLabelFontSize, }) + TICK_PADDING; - useEffect(() => { - if (ref.current) { - const axis = axisLeft(state.yScale) - .ticks(ticks) - .tickSizeInner(-state.bounds.chartWidth) - .tickFormat(tickFormat) - .tickPadding(TICK_PADDING); - const g = renderContainer(ref.current, { - id: "axis-height-linear", - transform: `translate(${margins.left} ${margins.top})`, - transition: { enable: enableTransition, duration: transitionDuration }, - render: (g) => g.call(axis), - renderUpdate: (g, opts) => - maybeTransition(g, { - transition: opts.transition, - s: (g) => g.call(axis), - }), - }); - - g.select(".domain").remove(); - g.selectAll(".tick line") - .attr("stroke", gridColor) - .attr("stroke-width", 1); - g.selectAll(".tick text") - .attr("dy", 3) - .attr("fill", labelColor) - .attr("font-family", fontFamily) - .style("font-size", labelFontSize) - .attr("text-anchor", "end"); - } - }, [ - state.bounds.chartWidth, - enableTransition, - fontFamily, - gridColor, - labelColor, - labelFontSize, - margins.left, - margins.top, - tickFormat, - ticks, - transitionDuration, - state.yScale, - ]); + useRenderAxisHeightLinear(ref, { + id: "axis-height-linear", + orientation: "left", + scale: state.yScale, + width: state.bounds.chartWidth, + height: state.bounds.chartHeight, + margins: state.bounds.margins, + lineColor: gridColor, + textColor: labelColor, + }); return ( <> @@ -125,11 +71,97 @@ export const AxisHeightLinear = () => { )} - + setRef(newRef)} /> ); }; +export const useRenderAxisHeightLinear = ( + container: SVGGElement | null, + { + id, + orientation, + scale, + width, + height, + margins, + lineColor, + textColor, + }: { + id: string; + orientation: "left" | "right"; + scale: ScaleLinear; + width: number; + height: number; + margins: { left: number; top: number }; + lineColor: string; + textColor: string; + } +) => { + const leftAligned = orientation === "left"; + const enableTransition = useTransitionStore((state) => state.enable); + const transitionDuration = useTransitionStore((state) => state.duration); + const { labelFontSize, fontFamily } = useChartTheme(); + const formatNumber = useFormatNumber({ decimals: "auto" }); + const calculationType = useInteractiveFilters((d) => d.calculation.type); + const normalized = calculationType === "percent"; + const ticks = getTickNumber(height); + const tickFormat = React.useCallback( + (d: NumberValue) => { + return normalized ? `${formatNumber(d)}%` : formatNumber(d); + }, + [formatNumber, normalized] + ); + + React.useEffect(() => { + if (!container) { + return; + } + + const axis = (leftAligned ? axisLeft : axisRight)(scale) + .ticks(ticks) + .tickSizeInner((leftAligned ? -1 : 1) * width) + .tickFormat(tickFormat) + .tickPadding(TICK_PADDING); + const g = renderContainer(container, { + id, + transform: `translate(${margins.left}, ${margins.top})`, + transition: { enable: enableTransition, duration: transitionDuration }, + render: (g) => g.call(axis), + renderUpdate: (g, opts) => + maybeTransition(g, { + transition: opts.transition, + s: (g) => g.call(axis), + }), + }); + + g.select(".domain").remove(); + g.selectAll(".tick line").attr("stroke", lineColor).attr("stroke-width", 1); + g.selectAll(".tick text") + .attr("dy", 3) + .attr("fill", textColor) + .attr("font-family", fontFamily) + .style("font-size", labelFontSize) + .attr("text-anchor", leftAligned ? "end" : "start"); + }, [ + container, + enableTransition, + fontFamily, + id, + labelFontSize, + leftAligned, + lineColor, + margins.left, + margins.top, + scale, + textColor, + tickFormat, + ticks, + transitionDuration, + width, + ]); +}; + export const AxisHeightLinearDomain = () => { const ref = useRef(null); const enableTransition = useTransitionStore((state) => state.enable);