Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div id="root"></div>
              
            
!

CSS

              
                html, body, #root {
    padding: 0;
    margin: 0;
    width: 100%;
    height: 100%;
    
    font-family: sans-serif;
}

.w-full {
    width: 100%
}

.h-full {
    height: 100%;
}

.axis-label {
    fill: black;
}

.axis-tick {
    stroke: black;
}

.grid-line {
    stroke: lightgray;
}

              
            
!

JS

              
                /**
 * using d3-zoom with react in a way that 'just works'
 * (this is not rocket science, but i struggled with it for a bit and did not find
 * other good resources on it)
 *
 * inspired by https://codepen.io/likr/pen/vYmBEPE, which implements the zooming
 * through CSS transform scale. that doesn't play well with axes/grid/etc. since 
 * d3 doesn't know about the transform (so eg. ticks would be a pain to get working 
 * nicely)
 * 
 * the important part is in App, i have added a comment block that explains it briefly
 */

const getRandomIntBetween = (min, max) => {
    const minCeiled = Math.ceil(min);
    const maxFloored = Math.floor(max);
    return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled);
};

const generateRandomDataPoints = (num, rangeX, rangeY) => {
    const dataPoints = [];
    for (let i = 0; i < num; i++) {
        dataPoints.push({
            x: getRandomIntBetween(...rangeX),
            y: getRandomIntBetween(...rangeY)
        });
    }
    return dataPoints;
};

const useResizeObserver = (elementRef) => {
    const [width, setWidth] = React.useState(0);
    const [height, setHeight] = React.useState(0);

    const resizeObserver = React.useMemo(() => (
        new ResizeObserver((entries) => {
            if (entries.length !== 1 || entries[0].borderBoxSize.length !== 1) {
                console.warn('resize observer returned unexpected entries', entries);
                return;
            }
            
            setWidth(entries[0].borderBoxSize[0].inlineSize);
            setHeight(entries[0].borderBoxSize[0].blockSize);
        })),
        []
    );

    React.useEffect(() => {
        if (elementRef.current != null) {
            resizeObserver.observe(elementRef.current);
        }

        return () => {
            resizeObserver.disconnect();
        };
    }, [elementRef.current]);

    return { width, height };
};

const Axis = ({ scale, position, dimensions, height = 10 }) => {
    const ticks = scale.ticks();

    return (
        <g>
            {ticks.map((tick) => (
                <g
                    key={tick}
                    transform={`translate(${position === 'bottom' ? scale(tick) : 0}, ${position === 'bottom' ? dimensions.height - height : scale(tick)})`}
                >
                    <text
                        x={position === 'bottom' ? 0 : 20}
                        y={position === 'bottom' ? -20 : 0}
                        dy={position === 'bottom' ? '0.71em' : '0.32em'}
                        textAnchor={position === 'bottom' ? 'middle' : 'start'}
                        className={'axis-label'}
                    >
                        {tick}
                    </text>
                    <line
                        x2={position === 'bottom' ? 0 : height}
                        y2={position === 'bottom' ? height : 0}
                        className={'axis-tick'}
                    />
                </g>
            ))}
        </g>
    );
};

const Grid = ({ xScale, yScale, dimensions }) => {
    const xTicks = xScale.ticks().map(xScale);
    const yTicks = yScale.ticks().map(yScale);
    
    return (<>
        {xTicks.map((tick) => <line key={tick} className={'grid-line'} x1={tick} x2={tick} y2={dimensions.height} />)}
        {yTicks.map((tick) => <line key={tick} className={'grid-line'} y1={tick} x2={dimensions.width} y2={tick} />)}
    </>);
};

const DataPoint = ({ x, y }) => {
    return (
        <circle cx={x} cy={y} r={5} />
    );
}

const App = () => {
    const wrapperRef = React.useRef(null);
    const svgRef = React.useRef(null);
    
    const dimensions = useResizeObserver(wrapperRef);

    const [transform, setTransform] = React.useState(d3.zoomIdentity);

    const dataPoints = React.useMemo(() => (
        generateRandomDataPoints(40, [0, dimensions.width], [0, dimensions.height])
    ), [dimensions.width, dimensions.height]);

    /**
     * we set up initial linear scales based on the extent of our data here, these are 
     * our baseline at zero zoom/pan
     * in principle these could be anything you want, but it is useful to base them on the data
     */
    const initialXScale = React.useMemo(() => (
        d3.scaleLinear().domain(d3.extent(dataPoints, (it) => it.x)).range([0, dimensions.width])
    ), [dataPoints]);
    const initialYScale = React.useMemo(() => (
        d3.scaleLinear().domain(d3.extent(dataPoints, (it) => it.y)).range([dimensions.height, 0])
    ), [dataPoints]);

    /**
     * the actual scales that are used for drawing are then just these initial scales, rescaled 
     * by the current zoom/pan transform
     * since we use d3 primitives (scales) to implement our current position/zoom everything else
     * 'just works', since everything (in d3-space) is based on them
     */
    const xScale = transform.rescaleX(initialXScale);
    const yScale = transform.rescaleY(initialYScale);

    React.useEffect(() => {
        if (svgRef.current == null) {
            return;
        }

        /**
         * here we set up the zoom handler, helpfully the event already includes a transform which
         * we can just use as-is
         */
        const zoom = d3.zoom().on('zoom', (event) => {
            setTransform(event.transform);
        });

        d3.select(svgRef.current).call(zoom);
    }, []);

    return (
        <div className={'w-full h-full'} ref={wrapperRef}>
            <svg ref={svgRef} width={dimensions.width} height={dimensions.height}>
                <Grid xScale={xScale} yScale={yScale} dimensions={dimensions} />
                
                <Axis
                    scale={xScale}
                    position={'bottom'}
                    dimensions={dimensions}
                />

                <Axis
                    scale={yScale}
                    position={'left'}
                    dimensions={dimensions}
                />
                
                {dataPoints.map((dataPoint, i) => (
                    <DataPoint key={i} x={xScale(dataPoint.x)} y={yScale(dataPoint.y)} />
                ))}
            </svg>
        </div>
    );
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

              
            
!
999px

Console