[前端] 泡沫樹狀圖: Carrot(FoamTree)

Carrot
https://carrotsearch.com/

FoamTree
https://carrotsearch.com/foamtree/

FoamTree demo
https://get.carrotsearch.com/foamtree/latest/demos/

胡蘿蔔(Carrot)為文檔聚類分析平臺,通過其非常強大的聚類演算法將結果視覺化為泡沫樹(foamTree),常用於聚類分析與佔比視覺化,例如webpack打包後的chunks大小分析視覺化即為Carrot的FoamTree。


以下為單html檔測試範例(FoamTree圖)︰
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    <title>foamtree polygon Demo</title>

    <!--使用lodash-->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>

    <!--使用foamtree-->
    <script src="https://get.carrotsearch.com/foamtree/latest/carrotsearch.foamtree.js"></script>

    <style>
        body {
            background-color: black;
            font-family: Arial;
        }

        #app {
            position: absolute;
            top: 0;
            bottom: 0;
            left: 0;
            right: 0;
        }
    </style>

</head>
<body>

    <div id="app"></div>

    <script>

        let imgs = [
            'https://images.freeimages.com/images/large-previews/8a1/small-waterfall-1376352.jpg',
            'https://images.freeimages.com/images/large-previews/c53/yellowstone-river-1361768.jpg',
            'https://images.freeimages.com/images/large-previews/f2c/effi-1-1366221.jpg',
            'https://images.freeimages.com/images/large-previews/9f9/selfridges-2-1470748.jpg',
            'https://images.freeimages.com/images/large-previews/fa7/in-prayer-1313108.jpg',
            'https://images.freeimages.com/images/large-previews/371/swiss-mountains-1362975.jpg',
            'https://images.freeimages.com/images/large-previews/535/natural-wonders-1400924.jpg',
            'https://images.freeimages.com/images/large-previews/f5d/butterfly-1378183.jpg',
            'https://images.freeimages.com/images/large-previews/e2a/boise-downtown-1387405.jpg',
            'https://images.freeimages.com/images/large-previews/8a5/red-tulip-2-1401227.jpg',
            'https://images.freeimages.com/images/large-previews/bf4/the-road-through-the-woods-1449194.jpg',
            'https://images.freeimages.com/images/large-previews/4dc/street-1366583.jpg',
            'https://images.freeimages.com/images/large-previews/977/beach-1364350.jpg',
            'https://images.freeimages.com/images/large-previews/07a/beach-soft-light-1379401.jpg',
            'https://images.freeimages.com/images/large-previews/815/xmas-bunny-1313404.jpg',
            'https://images.freeimages.com/images/large-previews/ed3/a-stormy-paradise-1-1563744.jpg',
            'https://images.freeimages.com/images/large-previews/fb3/grass-1379193.jpg',
            'https://images.freeimages.com/images/large-previews/56e/hibiscus-1393855.jpg',
            'https://images.freeimages.com/images/large-previews/6d5/lake-at-the-cottage-1372381.jpg',
            'https://images.freeimages.com/images/large-previews/3a6/rain-on-sea-ii-1368899.jpg',
            'https://images.freeimages.com/images/large-previews/e71/frog-1371919.jpg',
            'https://images.freeimages.com/images/large-previews/b5a/dragon-fly-1391358.jpg',
            'https://images.freeimages.com/images/large-previews/901/butterfly-dress-1520606.jpg',
            'https://images.freeimages.com/images/large-previews/39a/no-grain-no-pain-1326753.jpg',
            'https://images.freeimages.com/images/large-previews/56c/salat-tomato-cheese-on-bread-1322759.jpg',
            'https://images.freeimages.com/images/large-previews/503/fruit-in-the-country-3-1323660.jpg',
            'https://images.freeimages.com/images/large-previews/bf2/fields-1-1370990.jpg',
            'https://images.freeimages.com/images/large-previews/e93/skyscrapers-1219500.jpg',
            'https://images.freeimages.com/images/large-previews/773/koldalen-4-1384902.jpg',
            'https://images.freeimages.com/images/large-previews/8b3/palm-and-pier-1390929.jpg',
            'https://images.freeimages.com/images/large-previews/5a3/milky-droplet-2-1190386.jpg',
            'https://images.freeimages.com/images/large-previews/4ad/snare-drum-second-take-1-1564542.jpg',
            'https://images.freeimages.com/images/large-previews/4f3/verde-que-te-quiero-verde-1-1408392.jpg',
            'https://images.freeimages.com/images/large-previews/313/coffee-1559191.jpg',
            'https://images.freeimages.com/images/large-previews/8a8/yellow-galben-1518220.jpg',
            'https://images.freeimages.com/images/large-previews/e7e/orange-portocaliu-1518994.jpg',
            'https://images.freeimages.com/images/large-previews/6bd/laundry-1522596.jpg',
            'https://images.freeimages.com/images/large-previews/2c8/glassy-1538117.jpg',
            'https://images.freeimages.com/images/large-previews/098/naive-art-paintings-naife-artworks-painting-raphael-perez-painter-landscape-artowkrs-1637257.jpg',
            'https://a-z-animals.com/media/animals/images/470x370/frog4.jpg',
            'https://www.spirit-animals.com/wp-content/uploads/2013/03/Frog-2-1024x768.jpg',
            'https://images.pexels.com/photos/162140/duckling-birds-yellow-fluffy-162140.jpeg?cs=srgb&dl=animal-beak-bird-162140.jpg&fm=jpg',
            'https://animals.sandiegozoo.org/sites/default/files/2016-08/category-thumbnail-mammals_0.jpg',
            'https://ichef.bbci.co.uk/images/ic/976x549/p04f5x5v.jpg',
            'https://cdn.theatlantic.com/assets/media/img/mt/2018/10/GettyImages_939413302/lead_720_405.jpg?mod=1541011155',
            'https://www.rspcansw.org.au/wp-content/uploads/2017/04/7_a-feature_adopt_mobile-1.jpg',
            'https://www.iata.org/whatwedo/cargo/PublishingImages/cargo_live_animals_parrot.jpg',
            'https://www.marwell.org.uk/images/main-images/white-rhino.jpg',
            'https://www.gvi.co.uk/wp-content/uploads/2016/05/2-1.png',
            'https://www.mlar.org/media/1165/dustin-1.jpg',
            'https://www.rd.com/wp-content/uploads/2018/06/Deer-fawn-portrait-760x506.jpg',
        ]

        //groups
        let groups = _.map(imgs, function (src) {
            let title = _.split(src, '/').pop()
            let group = {

                //title
                title: title,

                //per-group random number
                random: 0.8 + 0.2 * Math.random(),

                //Low-resolution image
                image: undefined,
                imageLoaded: false, //true when image has just been loaded
                imageLoadedTime: undefined, //time the image completed loading, used for fading-in animation

                //High-resolution image
                hiresImage: undefined,

                //True when some image-specific data is loading
                loading: false,

                //Low-resolution image loaded with CORS headers
                crossOriginImage: undefined

            };

            //load low-resolution image
            let img = new Image();
            img.onload = function () {
                group.image = img;
                group.imageLoaded = true;
                group.imageLoadedTime = Date.now();
                foamtree.redraw(false, src);
            };
            img.src = src

            return group;
        })

    </script>

    <script>

        //foamtree
        let foamtree = new CarrotSearchFoamTree({

            //id
            id: "app",

            //pixelRatio
            pixelRatio: window.devicePixelRatio || 1,

            //Use a simple fading animation
            rolloutDuration: 0,
            pullbackDuration: 0,
            fadeDuration: 300,

            //Make the polygon fill entire viewport when exposed
            groupExposureZoomMargin: 0.02,

            //Draw the attribution in dark colors to match the theme of this demo
            attributionTheme: "dark",

            //Draw custom content during animations
            wireframeContentDecorationDrawing: "always",

            //The decorator that draws our images and loading spinner animation.
            groupContentDecorator: function (opts, props, vars) {

                //The canvas 2d context on which we'll be drawing
                let ctx = props.context;

                //The group we're requested to draw.
                let group = props.group;

                //Current time, we'll need it to draw animations
                let now = Date.now();

                //Don't draw default labels and polygons
                vars.groupLabelDrawn = false;
                vars.groupPolygonDrawn = false;

                //Fading-in of the image that was just loaded and fading-out of the loading spinner animation.
                let imageAlpha = 0;
                if (group.image) {

                    //Image is available, fade it in
                    imageAlpha = Math.min(1, (now - group.imageLoadedTime) / 300);
                    ctx.globalAlpha = imageAlpha;
                    drawImage();

                }
                if (imageAlpha < 1 || group.loading) {

                    //Image still loading of fading-in, draw spinner animation.
                    ctx.globalAlpha = group.loading ? 1.0 : 1 - imageAlpha;
                    drawSpinner();

                    //Schedule a redraw of this group.
                    foamtree.redraw(false, group);

                }

                //Draws the loading spinner animation
                function drawSpinner() {
                    let cx = props.polygonCenterX;
                    let cy = props.polygonCenterY;

                    if (props.shapeDirty) {
                        //If group's polygon changed, recompute the radius of the inscribed polygon.
                        group.spinnerRadius = CarrotSearchFoamTree.geometry.circleInPolygon(props.polygon, cx, cy) * 0.1;
                    }

                    // Draw the spinner. Advance the animation based on the current time.
                    let angle = 2 * Math.PI * (now % 1000) / 1000;
                    ctx.beginPath();
                    ctx.arc(cx, cy, group.spinnerRadius, angle, angle + Math.PI / 5, true);
                    ctx.strokeStyle = "white";
                    ctx.lineWidth = group.spinnerRadius * 0.3;
                    ctx.stroke();

                }

                //Draws the image in the group's polygon.
                function drawImage() {
                    // If the group's polygon changed or image has just loaded, recompute the geometry-dependent elements.
                    if (props.shapeDirty || group.imageLoaded) {
                        group.imageLoaded = false;

                        //Bounding box of the polygon
                        group.boundingBox = CarrotSearchFoamTree.geometry.boundingBox(props.polygon);

                        //Rectangle inscribed in the polygon. We'll set the aspect ratio of the rectangle to be the
                        //same as the aspect ratio of the image. When the group is exposed, we'll draw the full
                        //image in the inscribed rectangle.
                        group.inscribedBox = CarrotSearchFoamTree.geometry.rectangleInPolygon(
                            props.polygon, props.polygonCenterX, props.polygonCenterY, group.image.width / group.image.height, 0.95);

                        //Check if there's enough space for the label. If not, shift the inscribed box upwards a bit.
                        let descriptionHeight = group.boundingBox.y + group.boundingBox.h - group.inscribedBox.y - group.inscribedBox.h;
                        let minDescriptionHeight = 0.125 * group.boundingBox.h;
                        if (descriptionHeight < minDescriptionHeight) {
                            group.inscribedBox = CarrotSearchFoamTree.geometry.rectangleInPolygon(
                                props.polygon, props.polygonCenterX, props.polygonCenterY - (minDescriptionHeight - descriptionHeight), group.image.width / group.image.height, 0.95);
                        }

                        //Clear the label buffer. We'll lay out the label when needed.
                        group.labelBuffer = null;
                    }

                    //Choose the right resolution image. We randomize the point at which we switch to the high-res image for each group
                    let image;
                    if (group.hiresImage &&
                        (group.boundingBox.w * props.viewportScale * group.random > group.image.width ||
                            group.boundingBox.h * props.viewportScale * group.random > group.image.height)) {
                        image = group.hiresImage;
                    } else {
                        image = group.image;
                    }

                    //animate the image rectangle during the expose animation.
                    let mainImageBox;
                    let exposure = props.exposure;
                    if (exposure <= 0) {
                        //Not exposed, render cropped image
                        mainImageBox = group.boundingBox;
                    } else if (exposure == 1) {
                        //Exposed, render full image
                        mainImageBox = group.inscribedBox;
                    } else {
                        //Expose animation in progress, transition the image rectangle geometry.
                        mainImageBox = {
                            x: group.boundingBox.x * (1 - exposure) + group.inscribedBox.x * exposure,
                            y: group.boundingBox.y * (1 - exposure) + group.inscribedBox.y * exposure,
                            w: group.boundingBox.w * (1 - exposure) + group.inscribedBox.w * exposure,
                            h: group.boundingBox.h * (1 - exposure) + group.inscribedBox.h * exposure
                        };
                    }

                    //Set the group polygon path on the drawing context.
                    ctx.beginPath();
                    props.polygonContext.replay(ctx);
                    ctx.closePath();

                    //Since the image is larger than the polygon, we'll need to apply clipping so that we don't draw beyond the polygon's area.
                    ctx.save();
                    ctx.clip();

                    //If the group is exposed, draw full image and the blurred backdrop.
                    if (exposure > 0) {
                        // Make the backdrop a bit darker.
                        ctx.fillStyle = "#000000";
                        ctx.fill();

                        //If blurred image is not available, draw a heavily cropped original image as the background and make it even darker.
                        ctx.save();
                        ctx.globalAlpha *= group.blurredImage ? 0.6 : 0.3;
                        drawImageInBox(group.blurredImage || group.image, group.boundingBox, group.blurredImage ? undefined : 2);
                        ctx.restore();

                        //When the expose animation is almost complete, draw some basic info about the photo.
                        if (exposure > 0.9) {

                            //re-layout text (which is costly) only when the geometry of the polygon changes.
                            if (!group.labelBuffer) {

                                //draw title
                                let title = group.title

                                //Create the label buffer
                                group.labelBuffer = ctx.scratch();

                                //Put the title line directly below the image rectangle.
                                let info = group.labelBuffer.fillPolygonWithText(props.polygon,
                                    props.polygonCenterX, group.inscribedBox.y + group.inscribedBox.h,
                                    title, { verticalAlign: "top", maxFontSize: group.boundingBox.h / 30, fontFamily: "Arial" });
                                if (info.fit) {
                                    // If there was enough space for the title, lay out author and views information.
                                    // Put those directly below the title.
                                    group.labelBuffer.save();
                                    group.labelBuffer.globalAlpha = 0.6;
                                    group.labelBuffer.fillPolygonWithText(props.polygon,
                                        props.polygonCenterX, info.box.y + info.box.h,
                                        '', { verticalAlign: "top", maxFontSize: info.fontSize, fontFamily: "Arial", verticalPadding: 0 });
                                    group.labelBuffer.restore();
                                }
                            }

                            //Fade-in the label
                            ctx.fillStyle = "rgba(255, 255, 255, " + (8.5 * (exposure - 0.9)).toFixed(2) + ")";
                            group.labelBuffer.replay(ctx);

                        }
                    }

                    //Draw the main image
                    if (exposure > 0 || props.hovered) {
                        drawImageInBox(image, mainImageBox);
                    } else {
                        ctx.save();
                        ctx.globalAlpha *= 0.9;
                        drawImageInBox(image, mainImageBox);
                        ctx.restore();
                    }
                    ctx.restore();

                    //Draw a subtle polygon outline
                    ctx.strokeStyle = props.exposure > 0 || props.hovered ? "rgba(255, 255, 255, 0.25)" : "rgba(0, 0, 0, 0.4)";
                    ctx.lineWidth = 1;
                    ctx.stroke();

                    //Draws the image positioned in the provided rectangle.
                    function drawImageInBox(image, box, multiplier) {
                        let groupWidthToHeight = box.w / box.h;
                        let imageWidthToHeight = image.width / image.height;

                        let scale = groupWidthToHeight < imageWidthToHeight ?
                            box.h / image.height : box.w / image.width;

                        let xOffset = box.x / scale, yOffset = box.y / scale;
                        if (groupWidthToHeight < imageWidthToHeight) {
                            scale = box.h / image.height;
                            xOffset -= (image.width - box.w / scale) / 2;
                        } else {
                            scale = box.w / image.width;
                            yOffset -= (image.height - box.h / scale) / 2;
                        }

                        group.scale = scale;

                        ctx.save();
                        ctx.scale(scale, scale);
                        ctx.translate(xOffset, yOffset);
                        if (multiplier) {
                            ctx.translate(image.width / 2, image.height / 2);
                            ctx.scale(multiplier, multiplier);
                            ctx.translate(-image.width / 2, -image.height / 2);
                        }
                        ctx.drawImage(image, 0, 0);
                        ctx.restore();
                    }

                }
            },

            //Call group content decorator on every polygon draw.
            groupContentDecoratorTriggering: "onSurfaceDirty",

            //Expose group on single click
            onGroupClick: function (e) {
                e.preventDefault();

                //Unexpose if clicked outside of any polygon
                if (!e.group) {
                    this.set("exposure", null);
                    return;
                }

                switch (this.get("state", e.group).exposure) {
                    case -1:
                    case 0:
                        //Clicked group is not exposed: compute blurred image and expose.
                        if (e.group.crossOriginImage && !e.group.blurredImage) {
                            e.group.blurredImage = Blur.stackBlur(e.group.crossOriginImage, 16);
                        }
                        this.set("exposure", e.group);
                        break;

                    case 1:
                        //Clicked group is exposed, unexpose.
                        this.set("exposure", null);
                        break;
                }
            },

            //Prevent default action (expose) on double click
            onGroupDoubleClick: function (e) {
                e.preventDefault();
            }

        });

        //set
        foamtree.set("dataObject", { groups: groups });

        //Resize on window size changes
        window.addEventListener("resize", function () {
            _.delay(function () {
                foamtree.resize()
            }, 300)
        });

    </script>

</body>
</html>


載入後畫面為︰



#Javascript, Carrot, FoamTree, 泡沫樹

留言