Skip to content

Commit

Permalink
refactor: Share some logic between AxisHeightLinear and AxisHeightLin…
Browse files Browse the repository at this point in the history
…earDual
  • Loading branch information
bprusinowski committed Oct 4, 2023
1 parent 294b12a commit 4e05b32
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 136 deletions.
90 changes: 22 additions & 68 deletions app/charts/combo/axis-height-linear-dual.tsx
Original file line number Diff line number Diff line change
@@ -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<SVGGElement>(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<SVGGElement | null>(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 (
<>
Expand All @@ -107,8 +62,7 @@ export const AxisHeightLinearDual = (props: AxisHeightLinearDualProps) => {
<span style={{ fontSize: axisLabelFontSize }}>{axisTitle}</span>
</OpenMetadataPanelWrapper>
</foreignObject>

<g ref={ref} />
<g ref={(newRef) => setRef(newRef)} />
</>
);
};
168 changes: 100 additions & 68 deletions app/charts/shared/axis-height-linear.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<SVGGElement>(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<SVGGElement | null>(null);
const state = useChartState() as
| AreasState
| ColumnsState
Expand All @@ -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 (
<>
Expand All @@ -125,11 +71,97 @@ export const AxisHeightLinear = () => {
</OpenMetadataPanelWrapper>
</foreignObject>
)}
<g ref={ref} />
<g ref={(newRef) => setRef(newRef)} />
</>
);
};

export const useRenderAxisHeightLinear = (
container: SVGGElement | null,
{
id,
orientation,
scale,
width,
height,
margins,
lineColor,
textColor,
}: {
id: string;
orientation: "left" | "right";
scale: ScaleLinear<number, number>;
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<SVGGElement>(null);
const enableTransition = useTransitionStore((state) => state.enable);
Expand Down

0 comments on commit 4e05b32

Please sign in to comment.