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="app"></div>

<!--

3D animated chart: React, WebGL, CSS3D, mouse interactivity (try hovering over the bars!)

Libraries and sources:

- React
- ReGL (WebGL helper): http://regl.party/
- glMatrix (math): http://glmatrix.net/
- onecolor (RGB conversion): https://github.com/One-com/one-color
- ColourLovers palettes extracted by @mattdesl (https://twitter.com/mattdesl): https://github.com/Jam3/nice-color-palettes
- React Motion (animation): https://github.com/chenglou/react-motion
- Mockaroo (chart labels): http://www.mockaroo.com/
- Google Fonts Michroma

-->

              
            
!

CSS

              
                html, body, #app {
    width: 100%;
    height: 100%;
}

.main {
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    min-height: 100%;
    padding: 0 0 20px;
    justify-content: center;
    align-items: center;
    margin-top: -20px;

    > ._chart {
        flex: none;
        position: relative;
        width: 480px;
        height: 360px;
    }
    
    > button {
        flex: none;
        display: block;
        border: 1px solid #fff;
        width: 200px;
        height: 40px;
        padding: 0 0 4px;
        line-height: 36px;
        font-family: Michroma;
        font-size: 18px;
        color: #fff;
        background: none;
        border-radius: 5px;
        cursor: pointer;
        outline: none;
        text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
    }
}

.random-chart__hover-label {
    position: absolute;
    background: rgba(255, 255, 255, 0.9);
    color: #444;
    padding: 5px 5px 7px; // vertical alignment nudge
    border-radius: 5px;
    font-family: Michroma, Arial, sans-serif;
    font-size: 16px;
    transform: translate(-50%, -100%) translate(0, -8px);

    > ._arrow {
        position: absolute;
        top: 100%;
        left: 50%;
        margin-left: -8px;
        border-left: 8px solid transparent;
        border-right: 8px solid transparent;
        border-top: 8px solid rgba(255, 255, 255, 0.9);
    }
}

              
            
!

JS

              
                const onecolor = one.color;
const { Motion, spring } = ReactMotion;
const reglInit = createREGL;

function hex2vector(cssHex) {
    const pc = onecolor(cssHex);

    return vec3.fromValues(
        pc.red(),
        pc.green(),
        pc.blue()
    );
}

class BarChart3D extends React.PureComponent {
    constructor({
        values,
        width,
        height,
        palette
    }) {
        super();

        // copy value array and coerce to 0..1
        this._values = [].concat(values).map(
            value => Math.max(0, Math.min(1, value)) || 0
        );

        if (this._values.length < 1) {
            throw new Error('missing values');
        }

        this.state = {
            barIsActive: this._values.map(() => false),
            graphicsInitialized: false
        };

        this._width = width;
        this._height = height;

        this._chartAreaW = 500;
        this._chartAreaH = 300;
        this._barSpacing = 10;
        this._barExtraRadius = this._barSpacing * 0.3;
        this._patternSize = 50;

        this._regl = null; // initialized after first render

        // reusable computation elements
        this._barBaseVec2 = vec2.create();
        this._barTopVec3 = vec3.create();
    }

    _setBarIsActive(index, status) {
        // reduce bar status state into new instance
        this.setState(state => ({
            barIsActive: [].concat(
                state.barIsActive.slice(0, index),
                [ !!status ],
                state.barIsActive.slice(index + 1)
            )
        }));
    }

    _handleCanvasRef = (canvas) => {
        // when unlinking, help WebGL context get cleaned up
        if (!canvas) {
            this._regl.destroy();
            this._regl = null; // dereference just in case
            return;
        }

        // initialize graphics
        this._regl = reglInit({
            canvas: canvas
        });

        this._barCommand = this._regl({
            vert: `
                precision mediump float;

                uniform mat4 camera;
                uniform vec2 base;
                uniform float radius, height;

                attribute vec3 position;
                attribute vec3 normal;

                varying vec3 fragPosition;
                varying vec3 fragNormal;

                void main() {
                    float z = position.z * height;

                    fragPosition = vec3(
                        (position.xy + vec2(1.0, 1.0)) * radius,
                        z
                    );
                    fragNormal = normal;

                    gl_Position = camera * vec4(
                        base + position.xy * radius,
                        z,
                        1.0
                    );
                }
            `,

            frag: `
                precision mediump float;

                uniform vec3 baseColor, secondaryColor, highlightColor;
                uniform float height;
                uniform float highlight;
                uniform int patternIndex;
                uniform float patternSize;

                varying vec3 fragPosition;
                varying vec3 fragNormal;

                float stripePattern() {
                    return step(patternSize * 0.5, mod((
                        fragPosition.y
                        - fragPosition.x
                        + (height - fragPosition.z)
                    ), patternSize));
                }

                float stripe2Pattern() {
                    return step(patternSize * 0.5, mod((
                        fragPosition.x
                        - fragPosition.y
                        + (height - fragPosition.z)
                    ), patternSize));
                }

                float checkerPattern() {
                    vec3 cellPosition = vec3(0, 0, height) - fragPosition;
                    float cellSize = patternSize * 0.4;

                    vec3 cellIndex = cellPosition / cellSize;
                    float dotChoice = mod((
                        step(1.0, mod(cellIndex.x, 2.0))
                        + step(1.0, mod(cellIndex.y, 2.0))
                        + step(1.0, mod(cellIndex.z, 2.0))
                    ), 2.0);

                    return dotChoice;
                }

                float dotPattern() {
                    vec3 attachedPos = vec3(0, 0, height) - fragPosition;

                    float dotSize = patternSize * 0.3;
                    vec3 dotPosition = attachedPos + dotSize * 0.5;
                    float dotDistance = length(mod(dotPosition, dotSize) / dotSize - vec3(0.5));

                    vec3 dotIndex = dotPosition / dotSize;
                    float dotChoice = mod((
                        step(1.0, mod(dotIndex.x, 2.0))
                        + step(1.0, mod(dotIndex.y, 2.0))
                        + step(1.0, mod(dotIndex.z, 2.0))
                    ), 2.0);

                    return dotChoice * step(dotDistance, 0.5);
                }

                float pattern() {
                    if (patternIndex == 0) {
                        return stripePattern();
                    }

                    if (patternIndex == 1) {
                        return checkerPattern();
                    }

                    if (patternIndex == 2) {
                        return stripe2Pattern();
                    }

                    if (patternIndex == 3) {
                        return dotPattern();
                    }

                    return 0.0;
                }

                void main() {
                    vec3 pigmentColor = mix(baseColor, secondaryColor, pattern());

                    vec3 lightDir = vec3(-0.5, 0.5, 1.0); // non-normalized to ensure top is at 1
                    float light = max(0.0, dot(fragNormal, lightDir));

                    float highlightMix = 1.75 * max(0.0, min(0.5, highlight - 0.25)); // clip off bouncy edges of value range

                    gl_FragColor = vec4(mix(pigmentColor, highlightColor, highlightMix + (1.0 - highlightMix) * light), 1.0);
                }
            `,

            attributes: {
                position: this._regl.buffer([
                    [ -1, 1, 0 ], [ -1, -1, 0 ], [ -1, 1, 1 ], [ -1, -1, 1 ], // left face
                    [ -1, -1, 1 ], [ -1, -1, 0 ], // degen connector
                    [ -1, -1, 0 ], [ 1, -1, 0 ], [ -1, -1, 1 ], [ 1, -1, 1 ], // front face
                    [ 1, -1, 1 ], [ -1, -1, 1 ], // degen connector
                    [ -1, -1, 1 ], [ 1, -1, 1 ], [ -1, 1, 1 ], [ 1, 1, 1 ] // top face
                ]),

                normal: this._regl.buffer([
                    [ -1, 0, 0 ], [ -1, 0, 0 ], [ -1, 0, 0 ], [ -1, 0, 0 ], // left face
                    [ -1, 0, 0 ], [ 0, -1, 0 ], // degen connector
                    [ 0, -1, 0 ], [ 0, -1, 0 ], [ 0, -1, 0 ], [ 0, -1, 0 ], // front face
                    [ 0, -1, 0 ], [ 0, 0, 1 ], // degen connector
                    [ 0, 0, 1 ], [ 0, 0, 1 ], [ 0, 0, 1 ], [ 0, 0, 1 ] // top face
                ])
            },

            uniforms: {
                camera: this._regl.prop('camera'),
                base: this._regl.prop('base'),
                radius: this._regl.prop('radius'),
                height: this._regl.prop('height'),
                highlight: this._regl.prop('highlight'),
                patternIndex: this._regl.prop('patternIndex'),
                patternSize: this._regl.prop('patternSize'),
                baseColor: this._regl.prop('baseColor'),
                secondaryColor: this._regl.prop('secondaryColor'),
                highlightColor: this._regl.prop('highlightColor')
            },

            primitive: 'triangle strip',
            count: 4 + 2 + 4 + 2 + 4
        });

        this.setState({ graphicsInitialized: true });
    }

    // eslint-disable-next-line max-statements
    render() {
        const baseColor = hex2vector(this.props.baseColor);
        const secondaryColor = hex2vector(this.props.secondaryColor);
        const highlightColor = hex2vector(this.props.highlightColor);
        const labelColorCss = this.props.labelColor;

        // chart 3D layout
        const barCellSize = this._chartAreaW / this._values.length;
        const barRadius = Math.max(this._barSpacing / 2, barCellSize / 2 - this._barSpacing); // padding of 10px
        const startX = -barCellSize * (this._values.length - 1) / 2;

        // animation setup (as single instance to help render scene in one shot)
        const motionDefaultStyle = {};
        const motionStyle = {};

        this._values.forEach((value, index) => {
            const isActive = this.state.barIsActive[index];

            motionDefaultStyle[`v${index}`] = 0;
            motionStyle[`v${index}`] = spring(value, { stiffness: 320, damping: 12 });

            motionDefaultStyle[`r${index}`] = 0;
            motionStyle[`r${index}`] = spring(
                isActive ? this._barExtraRadius : 0, // @todo just animate in 0..1 range
                { stiffness: 600, damping: 18 }
            );
        });

        return <Chart3DScene
            viewportWidth={this._width}
            viewportHeight={this._height}
            distance={this._chartAreaH * 4}
            centerX={0}
            centerY={0}
            centerZ={this._chartAreaH / 2}
            canvasRef={this._handleCanvasRef}
            content3d={{
                [`translate3d(${-this._chartAreaW / 2}px, -40px, ${this._chartAreaH}px) rotateX(90deg)`]: (
                    this._values.map((value, index) => <div
                        key={index}
                        style={{
                            position: 'absolute',
                            top: 0,
                            left: 0,
                            width: '100px', // non-fractional size for better precision via scaling
                            height: this._chartAreaH + 'px',

                            // prevent from showing on mobile tap hover
                            // @todo reconsider for a11y
                            WebkitTapHighlightColor: 'rgba(0, 0, 0, 0)',

                            transformOrigin: '0 0',
                            transform: `translate(${index * barCellSize}px, 0px) scale(${barCellSize / 100}, 1)`
                        }}
                        onMouseEnter={() => { this._setBarIsActive(index, true); }}
                        onMouseLeave={() => { this._setBarIsActive(index, false); }}
                        onClick={() => {
                            if (this.props.onBarClick) {
                                this.props.onBarClick(index);
                            }
                        }}
                    />)
                ),
                // @todo carry out contents
                [`translate(${-this._chartAreaW / 2 + 10}px, -60px)`]: (
                    <div style={{
                        whiteSpace: 'nowrap',

                        fontFamily: 'Michroma, Arial, sans-serif',
                        fontSize: '32px',
                        lineHeight: 1,
                        letterSpacing: '-2px',
                        color: labelColorCss
                    }}>{this.props.xLabel}</div>
                ),

                // @todo carry out contents
                [`translate(${this._chartAreaW / 2 + 10}px, -40px) rotateX(90deg) rotateZ(90deg)`]: (
                    <div style={{
                        whiteSpace: 'nowrap',

                        fontFamily: 'Michroma, Arial, sans-serif',
                        fontSize: '40px',
                        lineHeight: 1,
                        letterSpacing: '-2px',
                        color: labelColorCss
                    }}>{this.props.yLabel}</div>
                )
            }}
        >{(cameraMat4) => <div style={{
            position: 'absolute',
            top: 0,
            left: 0,
            width: '100%',
            height: '100%',
            pointerEvents: 'none' // allow underlying hover areas to work as intended
        }}>
            {/* animate when ready to render */}
            {this.state.graphicsInitialized ? <Motion
                defaultStyle={motionDefaultStyle}
                style={motionStyle}
            >{motion => {
                // general rendering refresh
                this._regl.poll();

                // clear canvas
                // @todo set colour?
                this._regl.clear({
                    depth: 1
                });

                // chart bar display
                this._values.forEach((value, index) => {
                    const motionValue = motion[`v${index}`];
                    const motionExtraRadius = motion[`r${index}`];

                    vec2.set(this._barBaseVec2, (index * barCellSize) + startX, barRadius - 40);

                    // @todo sort out how the ReGL framebuffer clearing works with react-motion framerate
                    this._barCommand({
                        camera: cameraMat4,
                        base: this._barBaseVec2,
                        radius: barRadius + motionExtraRadius,
                        height: this._chartAreaH * motionValue,
                        highlight: motionExtraRadius / this._barExtraRadius,
                        patternIndex: index % 4,
                        patternSize: this._patternSize,
                        baseColor: baseColor,
                        secondaryColor: secondaryColor,
                        highlightColor: highlightColor
                    });
                });

                // manually flush
                this._regl._gl.flush();

                // no element actually displayed
                return null;
            }}</Motion> : null}

            {this._values.map((value, index) => {
                // position overlay content on bar top
                vec3.set(
                    this._barTopVec3,
                    (index * barCellSize) + startX,
                    barRadius - 40,
                    value * this._chartAreaH
                );

                vec3.transformMat4(this._barTopVec3, this._barTopVec3, cameraMat4);

                // convert from GL device space (-1 .. 1) to 2D CSS space
                const x = (0.5 + 0.5 * this._barTopVec3[0]) * 100;
                const y = (0.5 - 0.5 * this._barTopVec3[1]) * 100;

                const barContent = this.props.renderBar && this.props.renderBar(
                    index,
                    this.state.barIsActive[index]
                );

                // set up mouse listeners on overlay content to ensure hover continuity
                return <div
                    key={index}
                    style={{
                        position: 'absolute',
                        top: `${y}%`,
                        left: `${x}%`,
                        pointerEvents: 'auto' // restore interactivity
                    }}
                    onMouseEnter={() => { this._setBarIsActive(index, true); }}
                    onMouseLeave={() => { this._setBarIsActive(index, false); }}
                >{barContent || null}</div>;
            })}
        </div>}</Chart3DScene>;
    }
}

class Chart3DScene extends React.PureComponent {
    constructor() {
        super();

        // reusable computation elements
        this._cameraMat4 = mat4.create();
        this._cameraPositionVec3 = vec3.create();
    }

    render() {
        mat4.perspective(this._cameraMat4, 0.5, this.props.viewportWidth / this.props.viewportHeight, 1, this.props.distance * 2.5);

        // camera distance
        vec3.set(this._cameraPositionVec3, 0, 0, -this.props.distance);
        mat4.translate(this._cameraMat4, this._cameraMat4, this._cameraPositionVec3);

        // camera orbit pitch and yaw
        mat4.rotateX(this._cameraMat4, this._cameraMat4, -1.0);
        mat4.rotateZ(this._cameraMat4, this._cameraMat4, Math.PI / 6);

        // camera offset
        vec3.set(this._cameraPositionVec3, -this.props.centerX, -this.props.centerY, -this.props.centerZ);
        mat4.translate(this._cameraMat4, this._cameraMat4, this._cameraPositionVec3);

        const cameraCssMat = `matrix3d(${this._cameraMat4.join(', ')})`;

        // not clipping contents on root div to allow custom overlay content to spill out
        return <div
            style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: '100%'
            }}
        >
            <canvas
                style={{
                    position: 'absolute',
                    top: 0,
                    left: 0,
                    width: '100%',
                    height: '100%'
                }}
                width={this.props.viewportWidth}
                height={this.props.viewportHeight}
                ref={this.props.canvasRef}
            />

            <div style={{
                position: 'absolute',
                zIndex: 0, // reset stacking context
                top: 0,
                left: 0,

                // apply camera matrix, center transform and emulate WebGL device coord range (-1, 1)
                transformStyle: 'preserve-3d'
            }} ref={(node) => {
                if (node) {
                    const realViewWidth = node.parentElement.offsetWidth;
                    const realViewHeight = node.parentElement.offsetHeight;

                    node.style.transform = `
                        translate(${realViewWidth / 2}px, ${realViewHeight / 2}px)
                        scale(${realViewWidth / 2}, ${-realViewHeight / 2})
                        ${cameraCssMat}
                    `;
                }
            }}>
                {Object.keys(this.props.content3d).map(modelTransform => <div
                    key={modelTransform}
                    style={{
                        position: 'absolute',
                        top: 0,
                        left: 0,

                        // transform in the XY plane, flipping first
                        transformStyle: 'preserve-3d',
                        transformOrigin: '0 0',
                        transform: `${modelTransform} scale(1, -1)`
                    }}
                >{this.props.content3d[modelTransform]}</div>)}
            </div>

            {/* custom overlay content */}
            {this.props.children(this._cameraMat4, cameraCssMat)}
        </div>;
    }
}

// from npmjs/nice-color-palettes
const colorPalettes = [
    ["#69d2e7","#a7dbd8","#e0e4cc","#f38630","#fa6900"],["#fe4365","#fc9d9a","#f9cdad","#c8c8a9","#83af9b"],["#ecd078","#d95b43","#c02942","#542437","#53777a"],["#556270","#4ecdc4","#c7f464","#ff6b6b","#c44d58"],["#774f38","#e08e79","#f1d4af","#ece5ce","#c5e0dc"],
    ["#e8ddcb","#cdb380","#036564","#033649","#031634"],["#490a3d","#bd1550","#e97f02","#f8ca00","#8a9b0f"],["#594f4f","#547980","#45ada8","#9de0ad","#e5fcc2"],["#00a0b0","#6a4a3c","#cc333f","#eb6841","#edc951"],["#e94e77","#d68189","#c6a49a","#c6e5d9","#f4ead5"]
]

const mockStockList = [
    'Trading YTD',
    'Last Session',
    'After Hours'
];

const mockSalesList = [
    'Q1 Prev Year',
    'Q3 This Year',
    'YoY Change',
    'Historical'
];

const mockRequestMetricList = [
    'Response',
    'IO Wait',
    'Peak Lag'
];

// credit: https://gist.github.com/blixt/f17b47c62508be59987b
function randomize(seed) {
    const intSeed = seed % 2147483647;
    const safeSeed = intSeed > 0 ? intSeed : intSeed + 2147483646;
    return safeSeed * 16807 % 2147483647;
}

function getRandomizedFraction(seed) {
    return (seed - 1) / 2147483646;
}

class RandomChart extends React.PureComponent {
    constructor(props) {
        super();

        // seeded random generation for predictable results
        const startSeed = Math.floor(Math.random() * 1000000);
        const random1 = randomize(startSeed);
        const random2 = randomize(random1);
        const random3 = randomize(random2);
        const random4 = randomize(random3);

        const mode = getRandomizedFraction(random1);
        const textSelector = getRandomizedFraction(random2);
        const paletteSelector = getRandomizedFraction(random3);
        const seriesLengthSelector = getRandomizedFraction(random4);

        this._textInfo = null;

        this._idNumber = random2 % 100000;

        if (mode < 0.2) {
            this._textInfo = {
                xLabel: 'STOCK: ' + mockStockList[Math.floor(textSelector * mockStockList.length)],
                yLabel: 'PRICE'
            };
        } else if (mode < 0.6) {
            this._textInfo = {
                xLabel: 'SALES: ' + mockSalesList[Math.floor(textSelector * mockSalesList.length)],
                yLabel: 'VOLUME'
            };
        } else {
            this._textInfo = {
                xLabel: 'REQUEST: ' + mockRequestMetricList[Math.floor(textSelector * mockRequestMetricList.length)],
                yLabel: 'TIME (ms)'
            };
        }

        // start with default palette at first, then randomize
        const paletteIndex = Math.floor(paletteSelector * colorPalettes.length);
        this._palette = colorPalettes[paletteIndex];

        this._series = Array(...new Array(3 + Math.floor(seriesLengthSelector * 10))).reduce(
            (itemSeedList) => {
                const prevSeed = itemSeedList.length > 0 ? itemSeedList[itemSeedList.length - 1] : random4;
                return itemSeedList.concat([ randomize(prevSeed) ]);
            },
            []
        ).map(itemSeed => getRandomizedFraction(itemSeed));
    }

    render() {
        // update body background
        document.body.style.background = this._palette[0];

        return (
            <BarChart3D
                values={this._series}
                width={480}
                height={360}
                xLabel={this._textInfo.xLabel}
                yLabel={this._textInfo.yLabel}
                baseColor={this._palette[3]}
                secondaryColor={this._palette[4]}
                highlightColor={this._palette[2]}
                labelColor={this._palette[1]}
                renderBar={(index, isActive) => isActive
                    ? <span className="random-chart__hover-label">
                        <span className="_arrow" />

                        {'0.' + Math.floor(100 + this._series[index] * 100).toString().slice(-2)}
                    </span>
                    : null
                }
            />
        );
    }
}


class Main extends React.PureComponent {
    constructor() {
        super();

        this.state = {
            count: 0
        };
    }
    
    render() {
        return <div className="main">
            <div className="_chart"><RandomChart key={this.state.count} /></div>
            <button
                type="button"
                onClick={() => this.setState(state => (
                    { count: state.count + 1 }
                ))}
            >Generate</button>
        </div>;
    }
}

WebFont.load({
    google: {
        families: [ 'Michroma' ]
    },

    active: function () {
        ReactDOM.render(
            React.createElement(Main),
            document.getElementById('app')
        );
    }
});

              
            
!
999px

Console