Commit 928486a6 authored by Yoon, Daeki's avatar Yoon, Daeki 😅
Browse files

chart 시작

parent bff0b7f5
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, { useEffect, useRef, useState } from "react";
import { scaleLinear } from "d3-scale";
import { max, min, axisBottom, select } from "d3";
import { generateRandomDataset } from "../helpers";
export const AxisCirclesD3 = () => {
const [dataset, setDataset] = useState(generateRandomDataset(30, 1));
const ref = useRef(null);
const width = 100,
height = 50,
r = 1;
const xPad = 3,
yPad = 3;
const xMin = min(dataset, (d) => d[0]) || 0;
const xMax = max(dataset, (d) => d[0]) || 100;
const yMin = min(dataset, (d) => d[1]) || 0;
const yMax = max(dataset, (d) => d[1]) || 50;
useEffect(() => {
const xScale = scaleLinear()
.domain([xMin, xMax])
.range([0 + xMin, width - xPad]);
const yScale = scaleLinear()
.domain([yMin, yMax])
.range([yMin, height - yPad]);
const rScale = scaleLinear()
.domain([xMin + yMin, xMax + yMax])
.range([0, height / 10]);
const svgElmt = select(ref.current);
const xAxis = axisBottom(xScale);
svgElmt.append("g").call(xAxis);
}, []);
return (
<div style={{ backgroundColor: "gray" }}>
<svg ref={ref} viewBox={`0 0 ${width + xMin} ${height + 3}`}></svg>
</div>
);
};
import { Axis, Orient } from "./Axis";
import { BottomAxis } from "./BottomAxis";
import rawData from "../data/data.json";
import { scaleUtc, scaleLinear } from "d3-scale";
import { line } from "d3-shape";
import { max, extent } from "d3";
import useResizeObserver from "../hooks/useResizeObserver";
import { useSize } from "../hooks/useSize";
import { useRef } from "react";
import { useChartDimensions } from "../hooks/useChartDimensions";
import { Comment } from "../commons";
type Record = {
date: Date;
value: number;
};
const data = rawData.map((d: any) => {
return {
date: new Date(d.date),
value: +d.value,
};
}) as Record[];
export const AxisExamples = () => {
const [target, dimensions] = useChartDimensions({
marginTop: 20,
marginBottom: 30,
marginLeft: 30,
marginRight: 20,
});
// console.log("dimensions height=", dimensions.height);
// const target = useRef(null);
// const size = useSize(target);
// console.log("size height=", size?.height);
// const width = 600;
// const height = 400;
const width = dimensions.width || 600;
const height = dimensions.height || 400;
const margin = {
top: dimensions.marginTop,
right: dimensions.marginRight,
bottom: dimensions.marginBottom,
left: dimensions.marginLeft,
};
// const width = size?.width || 600;
// const height = size?.height || 400;
// const margin = {
// top: 20,
// right: 20,
// bottom: 30,
// left: 30,
// };
const x = scaleUtc()
.domain(extent(data, (d) => d.date) as [Date, Date])
.range([margin.left, width - margin.right]);
const y = scaleLinear<number>()
.domain([0, max(data, (d) => d.value)] as [number, number])
.nice()
.range([height - margin.bottom, margin.top]);
const lineXY = line<Record>()
.defined((d) => !isNaN(d.value))
.x((d) => x(d.date))
.y((d) => y(d.value));
return (
<div>
<h1 className="text-3xl">Axis Examples</h1>
<Comment>Resizable element 적용</Comment>
<div ref={target} style={{ height: "80vh", border: "1px solid black" }}>
<svg width={width} height={height}>
<path
fill="none"
stroke="steelblue"
strokeWidth="1.5"
strokeLinejoin="round"
strokeLinecap="round"
d={lineXY(data) as string}
/>
<g transform={`translate(${margin.left},0)`}>
<Axis scale={y} orient={Orient.left} />
</g>
<g transform={`translate(0,${height - margin.bottom})`}>
<Axis scale={x} orient={Orient.bottom} />
</g>
</svg>
</div>
</div>
);
};
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 { AxisExamples } from "./AxisExamples";
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 { 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 { 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 { useRef, useState } from "react";
import { BarChart } from "./BarChart";
import { generateDataset1D } from "../helpers";
import { BarChartWithAnimation } from "./BarChartWithAnimation";
import { Button, Comment } from "../commons";
import { useSize } from "../hooks";
const initDataset = [
5, 10, 13, 19, 21, 25, 22, 18, 15, 13, 11, 12, 15, 20, 18, 17, 16, 18, 23, 25,
];
const nextDataset = [
21, 16, 6, 18, 10, 18, 20, 9, 17, 1, 5, 11, 18, 5, 2, 22, 7, 23, 3, 3,
];
export const BarExample = () => {
const [dataset, setDataset] = useState(initDataset);
const target = useRef(null);
const size = useSize(target);
const toggle = useRef(true);
const handleClick = () => {
setDataset(generateDataset1D());
};
const handleToggle = () => {
toggle.current ? setDataset(nextDataset) : setDataset(initDataset);
toggle.current = !toggle.current;
};
return (
<div>
<div className="my-4">
<Button className="mr-4" onClick={handleClick}>
새로운 무작위 데이터로 차트를 갱신하려면 클릭하세요.
</Button>
<Button onClick={handleToggle}>데이터 토글</Button>
</div>
<Comment>
애니메이션이 들어간 바차트. 문제는 텍스트가 같이 애니메이션이 안된다는
것입니다.
</Comment>
<BarChartWithAnimation dataset={dataset} />
<Comment>바차트</Comment>
<div ref={target}>
<BarChart dataset={dataset} dimensions={size} />
</div>
</div>
);
};
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 { BarExample } from "./BarExample";
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;
};
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