Commit 133b2de1 authored by Yoon, Daeki's avatar Yoon, Daeki 😅
Browse files

Merge branch 'chart'

parents 05ae28b3 622eecb4
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
"author": "Daeki Yoon", "author": "Daeki Yoon",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/d3": "^7.4.0",
"@types/react": "^18.0.14", "@types/react": "^18.0.14",
"@types/react-dom": "^18.0.5", "@types/react-dom": "^18.0.5",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
...@@ -29,7 +30,12 @@ ...@@ -29,7 +30,12 @@
"webpack-dev-server": "^4.9.2" "webpack-dev-server": "^4.9.2"
}, },
"dependencies": { "dependencies": {
"@juggle/resize-observer": "^3.4.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"d3": "^7.8.0",
"d3-axis": "^3.0.0",
"d3-scale": "^4.0.2",
"d3-shape": "^3.2.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.3.0" "react-router-dom": "^6.3.0"
......
import React from "react";
import type { AxisScale, AxisDomain } from "d3-axis";
function identity(x: any) {
return x;
}
/**
* Instead of a component for each orientation (like AxisLeft, AxisRight),
* we provide a value from this Orient object. Provide a value, like
* Orient.left, to the `orient` prop of the Axis component
* to place the axis on the left.
*/
export enum Orient {
top = 1,
right = 2,
bottom = 3,
left = 4,
}
function translateX(x: number) {
return "translate(" + x + ",0)";
}
function translateY(y: number) {
return "translate(0," + y + ")";
}
/**
* The axis component. This renders an axis, within a
* `g` element, for use in a chart.
*/
export const Axis = <Domain extends AxisDomain>({
scale,
ticks,
tickArguments = [],
tickValues = null,
tickFormat = null,
tickSize,
tickSizeInner = 6,
tickSizeOuter = 6,
tickPadding = 3,
tickTextProps = {},
tickLineProps = {},
domainPathProps = {},
orient = Orient.bottom,
offset = typeof window !== "undefined" && window.devicePixelRatio > 1
? 0
: 0.5,
}: {
/** An initialized d3 scale object, like a d3.linearScale */
scale: AxisScale<Domain>;
ticks?: any[];
tickArguments?: any[];
tickValues?: any[] | null;
tickFormat?: any;
tickSize?: number;
tickSizeInner?: number;
tickSizeOuter?: number;
tickPadding?: number;
/** Additional attributes to add to tick text elements, or null to omit */
tickTextProps?: React.SVGProps<SVGTextElement> | null;
/** Additional attributes to add to tick line elements, or null to omit */
tickLineProps?: React.SVGProps<SVGLineElement> | null;
/** Additional attributes to the domain path, or null to omit */
domainPathProps?: React.SVGProps<SVGPathElement> | null;
offset?: number;
orient?: Orient;
}) => {
if (tickSize) {
tickSizeInner = tickSize;
tickSizeOuter = tickSize;
}
if (ticks) {
tickArguments = ticks;
}
function number(scale: AxisScale<Domain>) {
return (d: any) => {
const value = scale(d);
return value === undefined ? 0 : +value;
};
}
function center(scale: AxisScale<Domain>, offset: number) {
if (scale.bandwidth) {
offset = Math.max(0, scale.bandwidth() - offset * 2) / 2;
}
if ((scale as any).round()) offset = Math.round(offset);
return (d: Domain) => {
const value = scale(d);
return value === undefined ? 0 : value + offset;
};
}
const k = orient === Orient.top || orient === Orient.left ? -1 : 1,
x = orient === Orient.left || orient === Orient.right ? "x" : "y",
transform =
orient === Orient.top || orient === Orient.bottom
? translateX
: translateY;
// Rendering
const values =
tickValues == null
? (scale as any).ticks
? (scale as any).ticks.apply(scale, tickArguments)
: scale.domain()
: tickValues,
format =
tickFormat == null
? "tickFormat" in scale
? (scale as any).tickFormat.apply(scale, tickArguments)
: identity
: tickFormat,
spacing = Math.max(tickSizeInner, 0) + tickPadding,
range = scale.range(),
range0 = +range[0] + offset,
range1 = +range[range.length - 1] + offset,
position = (scale.bandwidth ? center : number)(scale.copy(), offset);
const domainPath =
orient === Orient.left || orient === Orient.right
? tickSizeOuter
? "M" +
k * tickSizeOuter +
"," +
range0 +
"H" +
offset +
"V" +
range1 +
"H" +
k * tickSizeOuter
: "M" + offset + "," + range0 + "V" + range1
: tickSizeOuter
? "M" +
range0 +
"," +
k * tickSizeOuter +
"V" +
offset +
"H" +
range1 +
"V" +
k * tickSizeOuter
: "M" + range0 + "," + offset + "H" + range1;
const lineProps = {
[x + "2"]: k * tickSizeInner,
};
const textProps = {
[x]: k * spacing,
};
return (
<g>
{values.map((tick: any, i: number) => (
<g
className="tick"
key={i}
transform={transform(position(tick) + offset)}
>
{tickLineProps && (
<line stroke="currentColor" {...lineProps} {...tickLineProps} />
)}
{tickTextProps && (
<text
fill="currentColor"
dy={
orient === Orient.top
? "0em"
: orient === Orient.bottom
? "0.71em"
: "0.32em"
}
fontSize="10"
fontFamily="sans-serif"
textAnchor={
orient === Orient.right
? "start"
: orient === Orient.left
? "end"
: "middle"
}
{...textProps}
{...tickTextProps}
>
{format(tick)}
</text>
)}
</g>
))}
{domainPathProps && (
<path
className="domain"
stroke="currentColor"
fill="transparent"
d={domainPath}
{...domainPathProps}
/>
)}
</g>
);
};
import React, { useMemo } from "react";
import { ScaleLinear, scaleLinear } from "d3-scale";
type Props = {
x: number;
y: number;
scale: ScaleLinear<number, number>;
};
export const AxisScaleLinear = ({ x = 0, y = 0, scale }: Props) => {
const orient = "bottom";
const ticks = useMemo(
() =>
scale.ticks().map((value) => ({
value,
xOffset: scale(value),
})),
[]
);
// console.log("ticks:", ticks);
const range = scale.range();
return (
<g fontSize="10px" textAnchor="middle" transform={`translate(${x}, ${y})`}>
<path d={`M ${range[0]} 0.5 H ${range[1]}`} stroke="currentColor" />
{ticks.map(({ value, xOffset }) => (
<g key={value} transform={`translate(${xOffset}, 0)`}>
<line y2={6} stroke="currentColor" />
<text key={value} dy="1.5em">
{value}
</text>
</g>
))}
</g>
);
};
import React, { useMemo } from "react";
import { scaleLinear } from "d3-scale";
export const AxisV0 = () => {
const ticks = useMemo(() => {
const xScale = scaleLinear().domain([0, 100]).range([10, 290]);
return xScale.ticks().map((value) => ({
value,
xOffset: xScale(value),
}));
}, []);
return (
<svg>
<path d="M 9.5 0.5 H 290.5" stroke="currentColor" />
{ticks.map(({ value, xOffset }) => (
<g key={value} transform={`translate(${xOffset}, 0)`}>
<line y2={6} stroke="currentColor" />
<text
key={value}
style={{
fontSize: "10px",
textAnchor: "middle",
transform: "translateY(20px)",
}}
>
{value}
</text>
</g>
))}
</svg>
);
};
import React, { useMemo } from "react";
import { ScaleLinear, scaleLinear } from "d3-scale";
type Props = {
x: number;
y: number;
scale: ScaleLinear<number, number>;
};
export const AxisVerticalScaleLinear = ({ x = 0, y = 0, scale }: Props) => {
const orient = "left";
const ticks = useMemo(
() =>
scale.ticks().map((value) => ({
value,
yOffset: scale(value),
})),
[]
);
// console.log("ticks:", ticks);
const range = scale.range();
return (
<g fontSize="10px" textAnchor="middle" transform={`translate(${x}, ${y})`}>
<path d={`M 0.5 ${range[0]} V ${range[1]}`} stroke="currentColor" />
{ticks.map(({ value, yOffset }) => (
<g key={value} transform={`translate(0, ${yOffset})`}>
<line x2={-6} stroke="currentColor" />
<text key={value} dx="-1.5em" dy={"0.3em"}>
{value}
</text>
</g>
))}
</g>
);
};
import React from "react";
import { axisBottom } from "d3-axis";
import { scaleLinear } from "d3-scale";
export const BottomAxis = () => {
let scale = scaleLinear().domain([0, 100]).range([0, 500]);
let axis = axisBottom(scale);
console.log("axis:", axis);
return (
<div>
<h1>BottomAxis</h1>
<svg width="600" height="100">
<g transform="translate(20, 50)"></g>
</svg>
</div>
);
};
export { AxisScaleLinear } from "./AxisScaleLinear";
export { AxisVerticalScaleLinear } from "./AxisVerticalScaleLinear";
export { Axis, Orient } from "./Axis";
import { ScaleBand, ScaleLinear } from "d3-scale";
import React from "react";
type Props = {
dataset: number[];
height: number;
xScale: ScaleBand<number>;
yScale: ScaleLinear<number, number>;
};
export const Bar = ({ dataset, height, xScale, yScale }: Props) => {
// console.log("dataset:", dataset);
return (
<g>
{dataset.map((d, i) => {
console.log("d", d, "i", i, "x:", xScale(i), "y", yScale(d));
return (
<rect
key={i}
x={xScale(i)}
y={height - yScale(d)}
width={xScale.bandwidth()}
height={yScale(d)}
fill={`rgb(0, 0, ${Math.round(d * 10)})`}
></rect>
);
})}
</g>
);
};
import React from "react";
import { max, min, range } from "d3";
import { scaleLinear, scaleBand } from "d3-scale";
import { BarText } from "../texts";
import { Bar } from "./Bar";
type Props = {
dataset: number[];
dimensions: DOMRect | undefined;
};
export const BarChart = ({ dataset, dimensions }: Props) => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
const width = dimensions?.width || 600;
const height = dimensions?.height || 300;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.bottom - margin.top;
console.log("width:", width, "height:", height);
const xScale = scaleBand<number>()
.domain(range(dataset.length))
.rangeRound([0, innerWidth])
.paddingInner(0.05);
const yScale = scaleLinear()
.domain([0, max(dataset) || 0])
.rangeRound([0, innerHeight]);
return (
<div>
<svg
width={width}
height={height}
// style={{ borderWidth: "2px", borderColor: "black" }}
>
<g transform={`translate(${margin.left},${margin.top})`}>
<Bar
dataset={dataset}
xScale={xScale}
yScale={yScale}
height={innerHeight}
/>
<BarText
dataset={dataset}
xScale={xScale}
yScale={yScale}
height={innerHeight}
/>
</g>
</svg>
</div>
);
};
import React from "react";
import { max, min, range } from "d3";
import { scaleLinear, scaleBand } from "d3-scale";
import { BarText } from "../texts";
import { BarWithAnimation } from "./BarWithAnimation";
type Props = {
dataset: number[];
};
export const BarChartWithAnimation = ({ dataset }: Props) => {
const margin = { top: 20, right: 30, bottom: 20, left: 30 };
const width = window.innerWidth - margin.left - margin.right;
const height = 300 - margin.bottom - margin.top;
const xScale = scaleBand<number>()
.domain(range(dataset.length))
.rangeRound([0, width])
.paddingInner(0.05);
const yScale = scaleLinear()
.domain([0, max(dataset) || 0])
.rangeRound([0, height]);
return (
<svg
width={width + margin.left + margin.right}
height={height + margin.bottom + margin.top}
style={{ borderWidth: "2px", borderColor: "black" }}
>
<BarWithAnimation
dataset={dataset}
xScale={xScale}
yScale={yScale}
height={height}
/>
<BarText
dataset={dataset}
xScale={xScale}
yScale={yScale}
height={height}
/>
</svg>
);
};
import { ScaleBand, ScaleLinear } from "d3-scale";
import React, { Fragment, useEffect, useRef } from "react";
type Props = {
dataset: number[];
height: number;
xScale: ScaleBand<number>;
yScale: ScaleLinear<number, number>;
};
export const BarWithAnimation = ({
dataset,
height,
xScale,
yScale,
}: Props) => {
// console.log("dataset:", dataset);
const preDataset = useRef(dataset);
useEffect(() => {
preDataset.current = dataset;
}, [dataset]);
const getStyle = (i: number) => {
const style = {
// fill: "red",
animationName: `inmoveleftright-${i}`,
animationDuration: "1s",
// animationDirection: "alternate",
animationIterationCount: "1",
// transformOrigin: "bottom",
};
return style;
};
return (
<g transform={`scale(1, -1) translate(0, ${-height})`}>
{dataset.map((d, i) => {
// console.log("d", d, "i", i, "x:", xScale(i), "y", yScale(d));
return (
<Fragment key={Math.random()}>
<style>
{`
@keyframes inmoveleftright-${i} {
0% {
height: ${yScale(preDataset.current[i])}px;
}
100% {
height: ${yScale(d)}px;
}
}
`}
</style>
<rect
x={xScale(i)}
y={0}
width={xScale.bandwidth()}
height={yScale(d)}
fill={`rgb(0, 0, ${Math.round(d * 10)})`}
style={getStyle(i)}
></rect>
</Fragment>
);
})}
</g>
);
};
export { Bar } from "./Bar";
export { BarChart } from "./BarChart";
export { useSize } from "./useSize";
/**
* 출처: https://github.com/73nko/advanced-d3/blob/master/13-using-d3-with-react-js/src/completed/Chart/utils.js
*/
import { useEffect, useState, useRef } from "react";
export const combineChartDimensions = (dimensions: any) => {
let parsedDimensions = {
marginTop: 40,
marginRight: 30,
marginBottom: 40,
marginLeft: 75,
...dimensions,
};
return {
...parsedDimensions,
boundedHeight: Math.max(
parsedDimensions.height -
parsedDimensions.marginTop -
parsedDimensions.marginBottom,
0
),
boundedWidth: Math.max(
parsedDimensions.width -
parsedDimensions.marginLeft -
parsedDimensions.marginRight,
0
),
};
};
export const useChartDimensions = (passedSettings: any) => {
const ref = useRef<HTMLElement>();
const dimensions = combineChartDimensions(passedSettings);
const [width, changeWidth] = useState(0);
const [height, changeHeight] = useState(0);
useEffect(() => {
if (dimensions.width && dimensions.height) return;
const element = ref.current;
const resizeObserver = new ResizeObserver((entries) => {
if (!Array.isArray(entries)) return;
if (!entries.length) return;
const entry = entries[0];
if (width !== entry.contentRect.width)
changeWidth(entry.contentRect.width);
if (height !== entry.contentRect.height)
changeHeight(entry.contentRect.height);
});
element && resizeObserver.observe(element);
return () => element && resizeObserver.unobserve(element);
}, [passedSettings, height, width, dimensions]);
const newSettings = combineChartDimensions({
...dimensions,
width: dimensions.width || width,
height: dimensions.height || height,
});
return [ref, newSettings];
};
/**
* 출처: https://github.com/jaredLunde/react-hook
*/
import { useRef, useEffect } from "react";
const useLatest = <T extends any>(current: T) => {
const storedValue = useRef(current);
useEffect(() => {
storedValue.current = current;
});
return storedValue;
};
export default useLatest;
/**
* 출처: https://github.com/jaredLunde/react-hook
*/
import React from "react";
const usePassiveLayoutEffect =
React[
typeof document !== "undefined" && document.createElement !== void 0
? "useLayoutEffect"
: "useEffect"
];
export default usePassiveLayoutEffect;
/**
* 출처: https://github.com/jaredLunde/react-hook
*/
import {
ResizeObserver as Polyfill,
ResizeObserverEntry,
} from "@juggle/resize-observer";
import useLayoutEffect from "./usePassiveLayoutEffect";
import useLatest from "./useLatest";
// 브라우저 ResizeObserver를 사용할 건지 폴리필을 사용할 건지 결정
const ResizeObserver =
typeof window !== "undefined" && "ResizeObserver" in window
? // @ts-ignore
window.ResizeObserver
: Polyfill;
/**
* A React hook that fires a callback whenever ResizeObserver detects a change to its size
*
* @param target A React ref created by `useRef()` or an HTML element
* @param callback Invoked with a single `ResizeObserverEntry` any time
* the `target` resizes
*/
function useResizeObserver<T extends HTMLElement>(
target: React.RefObject<T> | T | null,
callback: UseResizeObserverCallback
): Polyfill {
const resizeObserver = getResizeObserver();
const storedCallback = useLatest(callback);
useLayoutEffect(() => {
let didUnsubscribe = false;
const targetEl = target && "current" in target ? target.current : target;
if (!targetEl) return () => {};
function cb(entry: ResizeObserverEntry, observer: Polyfill) {
if (didUnsubscribe) return;
storedCallback.current(entry, observer);
}
resizeObserver.subscribe(targetEl as HTMLElement, cb);
return () => {
didUnsubscribe = true;
resizeObserver.unsubscribe(targetEl as HTMLElement, cb);
};
}, [target, resizeObserver, storedCallback]);
return resizeObserver.observer;
}
function createResizeObserver() {
let ticking = false;
let allEntries: ResizeObserverEntry[] = [];
const callbacks: Map<any, Array<UseResizeObserverCallback>> = new Map();
const observer = new ResizeObserver(
(entries: ResizeObserverEntry[], obs: Polyfill) => {
allEntries = allEntries.concat(entries);
if (!ticking) {
window.requestAnimationFrame(() => {
const triggered = new Set<Element>();
for (let i = 0; i < allEntries.length; i++) {
if (triggered.has(allEntries[i].target)) continue;
triggered.add(allEntries[i].target);
const cbs = callbacks.get(allEntries[i].target);
cbs?.forEach((cb) => cb(allEntries[i], obs));
}
allEntries = [];
ticking = false;
});
}
ticking = true;
}
);
return {
observer,
subscribe(target: HTMLElement, callback: UseResizeObserverCallback) {
observer.observe(target);
const cbs = callbacks.get(target) ?? [];
cbs.push(callback);
callbacks.set(target, cbs);
},
unsubscribe(target: HTMLElement, callback: UseResizeObserverCallback) {
const cbs = callbacks.get(target) ?? [];
if (cbs.length === 1) {
observer.unobserve(target);
callbacks.delete(target);
return;
}
const cbIndex = cbs.indexOf(callback);
if (cbIndex !== -1) cbs.splice(cbIndex, 1);
callbacks.set(target, cbs);
},
};
}
let _resizeObserver: ReturnType<typeof createResizeObserver>;
const getResizeObserver = () =>
!_resizeObserver
? (_resizeObserver = createResizeObserver())
: _resizeObserver;
export type UseResizeObserverCallback = (
entry: ResizeObserverEntry,
observer: Polyfill
) => any;
export default useResizeObserver;
import React from "react";
import useResizeObserver from "./useResizeObserver";
export const useSize = (target: React.RefObject<HTMLElement>) => {
const [size, setSize] = React.useState<DOMRectReadOnly>();
React.useLayoutEffect(() => {
setSize(target.current?.getBoundingClientRect());
}, [target]);
// Where the magic happens
useResizeObserver(target, (entry) => setSize(entry.contentRect));
return size;
};
import React from "react";
import { arc, DefaultArcObject, PieArcDatum } from "d3-shape";
// import { DefaultArcObject } from "d3";
interface Props {
arcData: DefaultArcObject & PieArcDatum<any>;
label?: string;
[rest: string]: any;
}
export const Arc = ({ arcData, label, ...rest }: Props) => {
console.log("rest:", rest);
const arcGenerator = arc();
arcGenerator.cornerRadius(5.2);
const centroid = arcGenerator.centroid({
...arcData,
// startAngle: arcData.startAngle,
// endAngle: arcData.endAngle,
});
const pathData = arcGenerator(arcData);
// console.log("arcdata:", arcData, "path data:", pathData);
return (
<>
<path d={pathData ?? ""} {...rest}></path>
{label && (
<text x={centroid[0]} y={centroid[1]} dy={"0.33em"}>
{label}
</text>
)}
</>
);
};
import { scaleLinear } from "d3-scale";
import { arc, pie } from "d3-shape";
import React from "react";
import { Arc } from "./Arc";
type Props = {
data: any;
};
export const Pie = ({ data }: Props) => {
const width = 300;
const height = 300;
const pieGenerator = pie();
const arcs = pieGenerator.padAngle(0.1)(data);
console.log(arcs);
return (
<div>
<h1>Pie</h1>
<svg width={width} height={height}>
<g
transform={`translate(${width / 2},${height / 2})`}
textAnchor="middle"
stroke="white"
>
{arcs.map((arc, i) => (
<Arc
key={i}
arcData={{ ...arc, innerRadius: 0, outerRadius: 100 }}
label={String(arc.value)}
fill={"blue"}
stroke={"red"}
strokeWidth={2}
/>
))}
</g>
</svg>
</div>
);
};
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment