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圖)︰
載入後畫面為︰
#Javascript, Carrot, FoamTree, 泡沫樹
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, 泡沫樹

留言
張貼留言