mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
feat: autovisualiser of structured data with mcp-ui (#4153)
This commit is contained in:
@@ -65,5 +65,6 @@ do_not_version/
|
||||
# Error log artifacts from mcp replay tests
|
||||
crates/goose/tests/mcp_replays/*errors.txt
|
||||
|
||||
.gromastate
|
||||
# Nix build output
|
||||
result
|
||||
@@ -32,6 +32,7 @@ fn get_display_name(extension_id: &str) -> String {
|
||||
match extension_id {
|
||||
"developer" => "Developer Tools".to_string(),
|
||||
"computercontroller" => "Computer Controller".to_string(),
|
||||
"autovisualiser" => "Auto Visualiser".to_string(),
|
||||
"memory" => "Memory".to_string(),
|
||||
"tutorial" => "Tutorial".to_string(),
|
||||
"jetbrains" => "JetBrains".to_string(),
|
||||
@@ -747,6 +748,11 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
|
||||
// TODO we'll want a place to collect all these options, maybe just an enum in goose-mcp
|
||||
"built-in" => {
|
||||
let extension = cliclack::select("Which built-in extension would you like to enable?")
|
||||
.item(
|
||||
"autovisualiser",
|
||||
"Auto Visualizer",
|
||||
"Data visualization and UI generation tools",
|
||||
)
|
||||
.item(
|
||||
"computercontroller",
|
||||
"Computer Controller",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use goose_mcp::{ComputerControllerRouter, DeveloperRouter, MemoryRouter, TutorialRouter};
|
||||
use goose_mcp::{
|
||||
AutoVisualiserRouter, ComputerControllerRouter, DeveloperRouter, MemoryRouter, TutorialRouter,
|
||||
};
|
||||
use mcp_server::router::RouterService;
|
||||
use mcp_server::{BoundedService, ByteTransport, Server};
|
||||
use tokio::io::{stdin, stdout};
|
||||
@@ -28,6 +30,7 @@ pub async fn run_server(name: &str) -> Result<()> {
|
||||
let router: Option<Box<dyn BoundedService>> = match name {
|
||||
"developer" => Some(Box::new(RouterService(DeveloperRouter::new()))),
|
||||
"computercontroller" => Some(Box::new(RouterService(ComputerControllerRouter::new()))),
|
||||
"autovisualiser" => Some(Box::new(RouterService(AutoVisualiserRouter::new()))),
|
||||
"memory" => Some(Box::new(RouterService(MemoryRouter::new()))),
|
||||
"tutorial" => Some(Box::new(RouterService(TutorialRouter::new()))),
|
||||
_ => None,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
@@ -0,0 +1,661 @@
|
||||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Prevents IE11 from highlighting tiles in blue */
|
||||
.leaflet-tile::selection {
|
||||
background: transparent;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer,
|
||||
.leaflet-container .leaflet-tile {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.leaflet-container img.leaflet-tile {
|
||||
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||
mix-blend-mode: plus-lighter;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
svg.leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive,
|
||||
svg.leaflet-image-layer.leaflet-interactive path {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover,
|
||||
.leaflet-bar a:focus {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover,
|
||||
.leaflet-control-attribution a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-attribution-flag {
|
||||
display: inline !important;
|
||||
vertical-align: baseline !important;
|
||||
width: 1em;
|
||||
height: 0.6669em;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
white-space: nowrap;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 1px 1px #fff;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 24px 13px 20px;
|
||||
line-height: 1.3;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
min-height: 1px;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 17px 0;
|
||||
margin: 1.3em 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-top: -1px;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
pointer-events: auto;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||
color: #757575;
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||
color: #585858;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
-ms-zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
||||
|
||||
/* Printing */
|
||||
|
||||
@media print {
|
||||
/* Prevent printers from removing background-images of controls. */
|
||||
.leaflet-control {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,334 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Interactive Chart</title>
|
||||
|
||||
<script>
|
||||
{{CHART_MIN}}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 2em;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.header p {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 30px;
|
||||
position: relative;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chart-container {
|
||||
padding: 15px;
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 id="chartTitle">📊 Chart Visualization</h1>
|
||||
<p id="chartSubtitle">Interactive data visualization</p>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<canvas id="mainChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Data will be injected here
|
||||
const chartData = {{CHART_DATA}};
|
||||
|
||||
let chart;
|
||||
|
||||
function getChartOptions(type) {
|
||||
const baseOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index'
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 20,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
titleColor: 'white',
|
||||
bodyColor: 'white',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
borderWidth: 1,
|
||||
cornerRadius: 8,
|
||||
displayColors: true,
|
||||
padding: 12
|
||||
},
|
||||
title: {
|
||||
display: false // We handle title in HTML
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
duration: 750,
|
||||
easing: 'easeInOutQuart'
|
||||
}
|
||||
};
|
||||
|
||||
if (type === 'scatter') {
|
||||
baseOptions.scales = {
|
||||
x: {
|
||||
type: 'linear',
|
||||
position: 'bottom',
|
||||
title: {
|
||||
display: chartData.xAxisLabel ? true : false,
|
||||
text: chartData.xAxisLabel || 'X Values',
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: chartData.yAxisLabel ? true : false,
|
||||
text: chartData.yAxisLabel || 'Y Values',
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
baseOptions.scales = {
|
||||
x: {
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
title: {
|
||||
display: chartData.xAxisLabel ? true : false,
|
||||
text: chartData.xAxisLabel,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
title: {
|
||||
display: chartData.yAxisLabel ? true : false,
|
||||
text: chartData.yAxisLabel,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Apply any custom options from the data
|
||||
if (chartData.options) {
|
||||
return mergeOptions(baseOptions, chartData.options);
|
||||
}
|
||||
|
||||
return baseOptions;
|
||||
}
|
||||
|
||||
function mergeOptions(base, custom) {
|
||||
// Simple deep merge for options
|
||||
const merged = {...base};
|
||||
for (const key in custom) {
|
||||
if (typeof custom[key] === 'object' && !Array.isArray(custom[key]) && custom[key] !== null) {
|
||||
merged[key] = mergeOptions(base[key] || {}, custom[key]);
|
||||
} else {
|
||||
merged[key] = custom[key];
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
// Default color palette
|
||||
const defaultColors = [
|
||||
'rgba(54, 162, 235, 0.8)', // Blue
|
||||
'rgba(255, 99, 132, 0.8)', // Red
|
||||
'rgba(75, 192, 192, 0.8)', // Teal
|
||||
'rgba(255, 159, 64, 0.8)', // Orange
|
||||
'rgba(153, 102, 255, 0.8)', // Purple
|
||||
'rgba(255, 206, 86, 0.8)', // Yellow
|
||||
'rgba(46, 204, 113, 0.8)', // Green
|
||||
'rgba(231, 76, 60, 0.8)', // Darker Red
|
||||
'rgba(52, 152, 219, 0.8)', // Light Blue
|
||||
'rgba(155, 89, 182, 0.8)', // Violet
|
||||
];
|
||||
|
||||
const defaultBorderColors = [
|
||||
'rgba(54, 162, 235, 1)',
|
||||
'rgba(255, 99, 132, 1)',
|
||||
'rgba(75, 192, 192, 1)',
|
||||
'rgba(255, 159, 64, 1)',
|
||||
'rgba(153, 102, 255, 1)',
|
||||
'rgba(255, 206, 86, 1)',
|
||||
'rgba(46, 204, 113, 1)',
|
||||
'rgba(231, 76, 60, 1)',
|
||||
'rgba(52, 152, 219, 1)',
|
||||
'rgba(155, 89, 182, 1)',
|
||||
];
|
||||
|
||||
function processDatasets(datasets, chartType) {
|
||||
return datasets.map((dataset, index) => {
|
||||
const processed = {...dataset};
|
||||
|
||||
// Add default colors if not provided
|
||||
if (!processed.backgroundColor) {
|
||||
if (chartType === 'bar' && !dataset.label && datasets.length === 1) {
|
||||
// For single bar dataset without label, use multiple colors
|
||||
processed.backgroundColor = defaultColors;
|
||||
processed.borderColor = defaultBorderColors;
|
||||
} else {
|
||||
processed.backgroundColor = defaultColors[index % defaultColors.length];
|
||||
processed.borderColor = defaultBorderColors[index % defaultBorderColors.length];
|
||||
}
|
||||
}
|
||||
|
||||
if (!processed.borderColor && processed.backgroundColor) {
|
||||
processed.borderColor = processed.backgroundColor.replace('0.8', '1');
|
||||
}
|
||||
|
||||
// Set default border width
|
||||
if (processed.borderWidth === undefined) {
|
||||
processed.borderWidth = chartType === 'line' ? 2 : 1;
|
||||
}
|
||||
|
||||
// For line charts, add default tension if not specified
|
||||
if (chartType === 'line' && processed.tension === undefined) {
|
||||
processed.tension = 0.4;
|
||||
}
|
||||
|
||||
// For line charts, set fill to false by default if not specified
|
||||
if (chartType === 'line' && processed.fill === undefined) {
|
||||
processed.fill = false;
|
||||
}
|
||||
|
||||
return processed;
|
||||
});
|
||||
}
|
||||
|
||||
function initChart() {
|
||||
const ctx = document.getElementById('mainChart').getContext('2d');
|
||||
|
||||
// Set title and subtitle if provided
|
||||
if (chartData.title) {
|
||||
document.getElementById('chartTitle').textContent = chartData.title;
|
||||
}
|
||||
if (chartData.subtitle) {
|
||||
document.getElementById('chartSubtitle').textContent = chartData.subtitle;
|
||||
}
|
||||
|
||||
const chartType = chartData.type || 'line';
|
||||
const processedDatasets = processDatasets(chartData.datasets, chartType);
|
||||
|
||||
const chartConfig = {
|
||||
type: chartType,
|
||||
data: {
|
||||
labels: chartData.labels || [],
|
||||
datasets: processedDatasets
|
||||
},
|
||||
options: getChartOptions(chartType)
|
||||
};
|
||||
|
||||
chart = new Chart(ctx, chartConfig);
|
||||
}
|
||||
|
||||
// Function to measure and report content size for iframe auto-resizing
|
||||
function reportContentSize() {
|
||||
const contentHeight = Math.max(
|
||||
document.body.scrollHeight,
|
||||
document.body.offsetHeight,
|
||||
document.documentElement.clientHeight,
|
||||
document.documentElement.scrollHeight,
|
||||
document.documentElement.offsetHeight
|
||||
);
|
||||
|
||||
// Send size change message to parent window (for MCP-UI iframe auto-resize)
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'ui-size-change',
|
||||
payload: {
|
||||
height: contentHeight
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
window.onload = function() {
|
||||
initChart();
|
||||
|
||||
// Report initial size
|
||||
setTimeout(reportContentSize, 100);
|
||||
|
||||
// Watch for size changes using ResizeObserver if available
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
reportContentSize();
|
||||
});
|
||||
resizeObserver.observe(document.body);
|
||||
resizeObserver.observe(document.documentElement);
|
||||
}
|
||||
|
||||
// Fallback: also report on window resize
|
||||
window.addEventListener('resize', reportContentSize);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,274 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Chord Diagram</title>
|
||||
|
||||
<script>
|
||||
{{D3_MIN}}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
#chord {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.group-arc {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chord {
|
||||
cursor: pointer;
|
||||
fill-opacity: 0.67;
|
||||
stroke: #000;
|
||||
stroke-width: 0.5px;
|
||||
}
|
||||
|
||||
.chord:hover {
|
||||
fill-opacity: 0.9;
|
||||
}
|
||||
|
||||
.group-label {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-anchor: middle;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<svg id="chord"></svg>
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Data will be injected here
|
||||
const chordData = {{CHORD_DATA}};
|
||||
|
||||
// Chord variables
|
||||
let svg, chord, arc, ribbon, tooltip;
|
||||
const width = 500;
|
||||
const height = 500;
|
||||
const outerRadius = Math.min(width, height) * 0.4;
|
||||
const innerRadius = outerRadius - 25;
|
||||
|
||||
// Color scale
|
||||
const colors = d3.scaleOrdinal(d3.schemeCategory10);
|
||||
|
||||
// Initialize SVG
|
||||
function initializeSVG() {
|
||||
svg = d3.select("#chord")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
svg.selectAll("*").remove();
|
||||
|
||||
const g = svg.append("g")
|
||||
.attr("transform", `translate(${width / 2},${height / 2})`);
|
||||
|
||||
chord = d3.chord()
|
||||
.padAngle(0.05)
|
||||
.sortSubgroups(d3.descending);
|
||||
|
||||
arc = d3.arc()
|
||||
.innerRadius(innerRadius)
|
||||
.outerRadius(outerRadius);
|
||||
|
||||
ribbon = d3.ribbon()
|
||||
.radius(innerRadius);
|
||||
|
||||
tooltip = d3.select("#tooltip");
|
||||
|
||||
return g;
|
||||
}
|
||||
|
||||
// Draw the Chord diagram
|
||||
function drawChord(g, data) {
|
||||
const chords = chord(data.matrix);
|
||||
|
||||
// Draw the groups (arcs around the circle)
|
||||
const group = g.append("g")
|
||||
.selectAll("g")
|
||||
.data(chords.groups)
|
||||
.enter().append("g")
|
||||
.attr("class", "group");
|
||||
|
||||
group.append("path")
|
||||
.attr("class", "group-arc")
|
||||
.style("fill", d => colors(d.index))
|
||||
.style("stroke", d => d3.rgb(colors(d.index)).darker())
|
||||
.attr("d", arc)
|
||||
.on("mouseover", function(event, d) {
|
||||
fadeChords(d.index, 0.1);
|
||||
showGroupTooltip(event, d, data.labels);
|
||||
})
|
||||
.on("mouseout", function() {
|
||||
fadeChords(null, 1);
|
||||
hideTooltip();
|
||||
});
|
||||
|
||||
// Add group labels
|
||||
group.append("text")
|
||||
.attr("class", "group-label")
|
||||
.each(d => { d.angle = (d.startAngle + d.endAngle) / 2; })
|
||||
.attr("dy", ".35em")
|
||||
.attr("transform", d => `
|
||||
rotate(${(d.angle * 180 / Math.PI - 90)})
|
||||
translate(${outerRadius + 10})
|
||||
${d.angle > Math.PI ? "rotate(180)" : ""}
|
||||
`)
|
||||
.style("text-anchor", d => d.angle > Math.PI ? "end" : null)
|
||||
.text(d => data.labels[d.index]);
|
||||
|
||||
// Draw the chords (ribbons between groups)
|
||||
g.append("g")
|
||||
.selectAll("path")
|
||||
.data(chords)
|
||||
.enter().append("path")
|
||||
.attr("class", "chord")
|
||||
.style("fill", d => colors(d.source.index))
|
||||
.attr("d", ribbon)
|
||||
.on("mouseover", function(event, d) {
|
||||
d3.select(this).style("fill-opacity", 0.9);
|
||||
showChordTooltip(event, d, data.labels, data.matrix);
|
||||
})
|
||||
.on("mouseout", function(event, d) {
|
||||
d3.select(this).style("fill-opacity", 0.67);
|
||||
hideTooltip();
|
||||
});
|
||||
}
|
||||
|
||||
// Fade chords for highlighting
|
||||
function fadeChords(focusIndex, opacity) {
|
||||
svg.selectAll(".chord")
|
||||
.style("opacity", d => {
|
||||
if (focusIndex === null) return 1;
|
||||
return d.source.index === focusIndex || d.target.index === focusIndex ? 1 : opacity;
|
||||
});
|
||||
|
||||
svg.selectAll(".group-arc")
|
||||
.style("opacity", d => {
|
||||
if (focusIndex === null) return 1;
|
||||
return d.index === focusIndex ? 1 : opacity;
|
||||
});
|
||||
}
|
||||
|
||||
// Tooltip functions
|
||||
function showGroupTooltip(event, d, labels) {
|
||||
const totalOut = d.value;
|
||||
const groupName = labels[d.index];
|
||||
|
||||
tooltip.style("opacity", 1)
|
||||
.html(`
|
||||
<strong>${groupName}</strong><br/>
|
||||
Total Flow: ${totalOut.toLocaleString()}<br/>
|
||||
Index: ${d.index}
|
||||
`)
|
||||
.style("left", (event.pageX + 10) + "px")
|
||||
.style("top", (event.pageY - 10) + "px");
|
||||
}
|
||||
|
||||
function showChordTooltip(event, d, labels, matrix) {
|
||||
const sourceName = labels[d.source.index];
|
||||
const targetName = labels[d.target.index];
|
||||
const sourceToTarget = matrix[d.source.index][d.target.index];
|
||||
const targetToSource = matrix[d.target.index][d.source.index];
|
||||
|
||||
tooltip.style("opacity", 1)
|
||||
.html(`
|
||||
<strong>Connection</strong><br/>
|
||||
${sourceName} → ${targetName}: ${sourceToTarget.toLocaleString()}<br/>
|
||||
${targetName} → ${sourceName}: ${targetToSource.toLocaleString()}<br/>
|
||||
<strong>Total: ${(sourceToTarget + targetToSource).toLocaleString()}</strong>
|
||||
`)
|
||||
.style("left", (event.pageX + 10) + "px")
|
||||
.style("top", (event.pageY - 10) + "px");
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
tooltip.style("opacity", 0);
|
||||
}
|
||||
|
||||
// Function to measure and report content size for iframe auto-resizing
|
||||
function reportContentSize() {
|
||||
// Get the actual content height
|
||||
const contentHeight = Math.max(
|
||||
document.body.scrollHeight,
|
||||
document.body.offsetHeight,
|
||||
document.documentElement.clientHeight,
|
||||
document.documentElement.scrollHeight,
|
||||
document.documentElement.offsetHeight
|
||||
);
|
||||
|
||||
// Send size change message to parent window (for MCP-UI iframe auto-resize)
|
||||
// The message format must match what the MCP-UI client expects
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'ui-size-change', // lowercase as expected by MCP-UI client
|
||||
payload: {
|
||||
height: contentHeight
|
||||
// width is optional, we're not setting it to allow responsive design
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and render on load
|
||||
window.onload = function() {
|
||||
const g = initializeSVG();
|
||||
drawChord(g, chordData);
|
||||
|
||||
// Report initial size
|
||||
setTimeout(reportContentSize, 100);
|
||||
|
||||
// Watch for size changes using ResizeObserver if available
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
reportContentSize();
|
||||
});
|
||||
resizeObserver.observe(document.body);
|
||||
resizeObserver.observe(document.documentElement);
|
||||
}
|
||||
|
||||
// Fallback: also report on window resize
|
||||
window.addEventListener('resize', reportContentSize);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,409 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Donut & Pie Charts</title>
|
||||
|
||||
<script>
|
||||
{{CHART_MIN}}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
font-size: 1.8em;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 350px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.legend-value {
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.stats-panel {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
margin-top: 30px;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 0.85em;
|
||||
color: #6c757d;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.stat-subtitle {
|
||||
font-size: 0.9em;
|
||||
color: #6c757d;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.charts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="charts-grid" id="chartsGrid">
|
||||
<!-- Charts will be dynamically inserted here -->
|
||||
</div>
|
||||
|
||||
<div class="stats-panel" id="statsPanel">
|
||||
<!-- Stats will be dynamically inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Data will be injected here
|
||||
const chartsData = {{CHARTS_DATA}};
|
||||
|
||||
// Default color palette
|
||||
const defaultColors = [
|
||||
'rgba(255, 99, 132, 1)', // Red
|
||||
'rgba(54, 162, 235, 1)', // Blue
|
||||
'rgba(75, 192, 192, 1)', // Teal
|
||||
'rgba(255, 159, 64, 1)', // Orange
|
||||
'rgba(153, 102, 255, 1)', // Purple
|
||||
'rgba(255, 206, 86, 1)', // Yellow
|
||||
'rgba(46, 204, 113, 1)', // Green
|
||||
'rgba(231, 76, 60, 1)', // Darker Red
|
||||
'rgba(52, 152, 219, 1)', // Light Blue
|
||||
'rgba(155, 89, 182, 1)', // Violet
|
||||
'rgba(241, 196, 15, 1)', // Gold
|
||||
'rgba(230, 126, 34, 1)', // Carrot
|
||||
'rgba(149, 165, 166, 1)', // Gray
|
||||
'rgba(26, 188, 156, 1)', // Turquoise
|
||||
'rgba(52, 73, 94, 1)', // Dark Blue-Gray
|
||||
];
|
||||
|
||||
function getColor(index, alpha = 1) {
|
||||
const color = defaultColors[index % defaultColors.length];
|
||||
return alpha === 1 ? color : color.replace('1)', `${alpha})`);
|
||||
}
|
||||
|
||||
function processChartData(data) {
|
||||
// Ensure we have an array of charts
|
||||
const charts = Array.isArray(data) ? data : [data];
|
||||
|
||||
return charts.map(chart => {
|
||||
// Process labels and data
|
||||
const labels = chart.labels || chart.data.map((d, i) => d.label || `Item ${i + 1}`);
|
||||
const values = chart.data.map(d => typeof d === 'object' ? d.value : d);
|
||||
|
||||
// Auto-assign colors if not provided
|
||||
const backgroundColors = chart.backgroundColor ||
|
||||
chart.data.map((d, i) => {
|
||||
if (typeof d === 'object' && d.color) return d.color;
|
||||
return getColor(i, 0.8);
|
||||
});
|
||||
|
||||
const borderColors = chart.borderColor ||
|
||||
backgroundColors.map(c => c.replace('0.8)', '1)'));
|
||||
|
||||
return {
|
||||
title: chart.title || 'Chart',
|
||||
type: chart.type || 'doughnut',
|
||||
labels: labels,
|
||||
values: values,
|
||||
backgroundColors: backgroundColors,
|
||||
borderColors: borderColors
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function createChart(chartData, canvasId) {
|
||||
const ctx = document.getElementById(canvasId).getContext('2d');
|
||||
|
||||
return new Chart(ctx, {
|
||||
type: chartData.type,
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
datasets: [{
|
||||
data: chartData.values,
|
||||
backgroundColor: chartData.backgroundColors,
|
||||
borderColor: chartData.borderColors,
|
||||
borderWidth: 2,
|
||||
hoverOffset: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false // We'll create custom legend
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
titleColor: 'white',
|
||||
bodyColor: 'white',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
borderWidth: 1,
|
||||
cornerRadius: 6,
|
||||
padding: 10,
|
||||
displayColors: true,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(1);
|
||||
return `${label}: ${value.toLocaleString()} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
animateRotate: true,
|
||||
animateScale: false,
|
||||
duration: 1000,
|
||||
easing: 'easeInOutQuart'
|
||||
},
|
||||
cutout: chartData.type === 'doughnut' ? '50%' : '0%'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createLegend(chartData, container) {
|
||||
const legendHtml = chartData.labels.map((label, index) => {
|
||||
const value = chartData.values[index];
|
||||
const total = chartData.values.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(1);
|
||||
|
||||
return `
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background-color: ${chartData.backgroundColors[index]}"></div>
|
||||
<span class="legend-label">${label}:</span>
|
||||
<span class="legend-value">${value.toLocaleString()} (${percentage}%)</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = legendHtml;
|
||||
}
|
||||
|
||||
function renderCharts() {
|
||||
const processedData = processChartData(chartsData);
|
||||
const chartsGrid = document.getElementById('chartsGrid');
|
||||
|
||||
// Clear existing content
|
||||
chartsGrid.innerHTML = '';
|
||||
|
||||
// Create chart elements
|
||||
processedData.forEach((chartData, index) => {
|
||||
const canvasId = `chart-${index}`;
|
||||
const legendId = `legend-${index}`;
|
||||
|
||||
const chartWrapper = document.createElement('div');
|
||||
chartWrapper.className = 'chart-wrapper';
|
||||
chartWrapper.innerHTML = `
|
||||
<div class="chart-title">${chartData.title}</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="${canvasId}"></canvas>
|
||||
</div>
|
||||
<div class="chart-legend" id="${legendId}"></div>
|
||||
`;
|
||||
|
||||
chartsGrid.appendChild(chartWrapper);
|
||||
|
||||
// Create the chart
|
||||
createChart(chartData, canvasId);
|
||||
|
||||
// Create the legend
|
||||
createLegend(chartData, document.getElementById(legendId));
|
||||
});
|
||||
|
||||
// Update statistics
|
||||
updateStats(processedData);
|
||||
}
|
||||
|
||||
function updateStats(processedData) {
|
||||
const statsPanel = document.getElementById('statsPanel');
|
||||
|
||||
// Calculate overall statistics
|
||||
let totalValue = 0;
|
||||
let totalCategories = 0;
|
||||
let maxValue = 0;
|
||||
let minValue = Infinity;
|
||||
|
||||
processedData.forEach(chart => {
|
||||
const chartTotal = chart.values.reduce((a, b) => a + b, 0);
|
||||
const chartMax = Math.max(...chart.values);
|
||||
const chartMin = Math.min(...chart.values);
|
||||
|
||||
totalValue += chartTotal;
|
||||
totalCategories += chart.values.length;
|
||||
maxValue = Math.max(maxValue, chartMax);
|
||||
minValue = Math.min(minValue, chartMin);
|
||||
});
|
||||
|
||||
const avgValue = totalCategories > 0 ? totalValue / totalCategories : 0;
|
||||
|
||||
statsPanel.innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Total Charts</div>
|
||||
<div class="stat-value">${processedData.length}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Total Value</div>
|
||||
<div class="stat-value">${totalValue.toLocaleString()}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Total Categories</div>
|
||||
<div class="stat-value">${totalCategories}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Average Value</div>
|
||||
<div class="stat-value">${avgValue.toFixed(0).toLocaleString()}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Max Value</div>
|
||||
<div class="stat-value">${maxValue.toLocaleString()}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">Min Value</div>
|
||||
<div class="stat-value">${minValue.toLocaleString()}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Function to measure and report content size for iframe auto-resizing
|
||||
function reportContentSize() {
|
||||
// Get the actual content height
|
||||
const contentHeight = Math.max(
|
||||
document.body.scrollHeight,
|
||||
document.body.offsetHeight,
|
||||
document.documentElement.clientHeight,
|
||||
document.documentElement.scrollHeight,
|
||||
document.documentElement.offsetHeight
|
||||
);
|
||||
|
||||
// Send size change message to parent window (for MCP-UI iframe auto-resize)
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'ui-size-change',
|
||||
payload: {
|
||||
height: contentHeight
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
window.onload = function() {
|
||||
renderCharts();
|
||||
|
||||
// Report initial size
|
||||
setTimeout(reportContentSize, 100);
|
||||
|
||||
// Watch for size changes using ResizeObserver if available
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
reportContentSize();
|
||||
});
|
||||
resizeObserver.observe(document.body);
|
||||
resizeObserver.observe(document.documentElement);
|
||||
}
|
||||
|
||||
// Fallback: also report on window resize
|
||||
window.addEventListener('resize', reportContentSize);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,279 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Interactive Map Visualization</title>
|
||||
|
||||
<style>
|
||||
{{LEAFLET_CSS}}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 2.5em;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.header p {
|
||||
margin: 0;
|
||||
font-size: 1.1em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border-radius: 0 0 12px 12px;
|
||||
}
|
||||
|
||||
.custom-popup {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.custom-popup h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.custom-popup p {
|
||||
margin: 5px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.marker-cluster-small {
|
||||
background-color: rgba(181, 226, 140, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster-small div {
|
||||
background-color: rgba(110, 204, 57, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster-medium {
|
||||
background-color: rgba(241, 211, 87, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster-medium div {
|
||||
background-color: rgba(240, 194, 12, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster-large {
|
||||
background-color: rgba(253, 156, 115, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster-large div {
|
||||
background-color: rgba(241, 128, 23, 0.6);
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.marker-cluster {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* MarkerCluster CSS */
|
||||
.marker-cluster-small div,
|
||||
.marker-cluster-medium div,
|
||||
.marker-cluster-large div {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
border-radius: 15px;
|
||||
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
{{LEAFLET_JS}}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
{{MARKERCLUSTER_JS}}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Data from the tool
|
||||
const mapData = {{MAP_DATA}};
|
||||
|
||||
// Initialize map
|
||||
let map;
|
||||
let markerClusterGroup;
|
||||
|
||||
function initMap() {
|
||||
// Determine initial center and zoom
|
||||
let initialLat = mapData.center ? mapData.center.lat : 39.8283;
|
||||
let initialLng = mapData.center ? mapData.center.lng : -98.5795;
|
||||
let initialZoom = mapData.zoom || 4;
|
||||
|
||||
map = L.map('map').setView([initialLat, initialLng], initialZoom);
|
||||
|
||||
// Add tile layers
|
||||
const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxZoom: 18
|
||||
});
|
||||
|
||||
const satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: '© Esri, © OpenStreetMap contributors',
|
||||
maxZoom: 18
|
||||
});
|
||||
|
||||
const topoLayer = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenTopoMap contributors',
|
||||
maxZoom: 17
|
||||
});
|
||||
|
||||
// Add default layer
|
||||
osmLayer.addTo(map);
|
||||
|
||||
// Layer control
|
||||
const baseLayers = {
|
||||
"OpenStreetMap": osmLayer,
|
||||
"Satellite": satelliteLayer,
|
||||
"Topographic": topoLayer
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
|
||||
// Initialize marker cluster group if clustering is enabled
|
||||
if (mapData.clustering !== false) {
|
||||
markerClusterGroup = L.markerClusterGroup({
|
||||
chunkedLoading: true,
|
||||
maxClusterRadius: mapData.clusterRadius || 50,
|
||||
iconCreateFunction: function(cluster) {
|
||||
const count = cluster.getChildCount();
|
||||
let className = 'marker-cluster-small';
|
||||
|
||||
if (count > 10) {
|
||||
className = 'marker-cluster-medium';
|
||||
}
|
||||
if (count > 100) {
|
||||
className = 'marker-cluster-large';
|
||||
}
|
||||
|
||||
return new L.DivIcon({
|
||||
html: '<div><span>' + count + '</span></div>',
|
||||
className: 'marker-cluster ' + className,
|
||||
iconSize: new L.Point(40, 40)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderMarkers() {
|
||||
const markers = mapData.markers || [];
|
||||
const maxValue = Math.max(...markers.map(m => m.value || 1), 1);
|
||||
|
||||
markers.forEach(point => {
|
||||
// Create custom icon based on value
|
||||
const value = point.value || 1;
|
||||
const size = Math.max(10, Math.min(30, (value / maxValue) * 30));
|
||||
const color = point.color || getColorByValue(value, maxValue);
|
||||
|
||||
const customIcon = L.divIcon({
|
||||
className: 'custom-marker',
|
||||
html: `<div style="
|
||||
background-color: ${color};
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: ${Math.max(8, size * 0.4)}px;
|
||||
">${point.label || (value > 1000 ? Math.round(value/1000) + 'k' : value)}</div>`,
|
||||
iconSize: [size, size],
|
||||
iconAnchor: [size/2, size/2]
|
||||
});
|
||||
|
||||
const marker = L.marker([point.lat, point.lng], {
|
||||
icon: point.useDefaultIcon ? new L.Icon.Default() : customIcon
|
||||
});
|
||||
|
||||
// Create popup content if provided
|
||||
if (point.popup || point.name || point.description) {
|
||||
const popupContent = point.popup || `
|
||||
<div class="custom-popup">
|
||||
${point.name ? `<h3>${point.name}</h3>` : ''}
|
||||
${point.value !== undefined ? `<p><strong>Value:</strong> ${point.value.toLocaleString()}</p>` : ''}
|
||||
<p><strong>Location:</strong> ${point.lat.toFixed(4)}, ${point.lng.toFixed(4)}</p>
|
||||
${point.description ? `<p><strong>Description:</strong> ${point.description}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popupContent);
|
||||
}
|
||||
|
||||
// Add to cluster group or directly to map
|
||||
if (markerClusterGroup) {
|
||||
markerClusterGroup.addLayer(marker);
|
||||
} else {
|
||||
marker.addTo(map);
|
||||
}
|
||||
});
|
||||
|
||||
// Add cluster group to map if using clustering
|
||||
if (markerClusterGroup) {
|
||||
map.addLayer(markerClusterGroup);
|
||||
}
|
||||
|
||||
// Fit map to marker bounds if autoFit is enabled
|
||||
if (mapData.autoFit !== false && markers.length > 0) {
|
||||
const group = new L.featureGroup(markers.map(m => L.marker([m.lat, m.lng])));
|
||||
map.fitBounds(group.getBounds().pad(0.1));
|
||||
}
|
||||
}
|
||||
|
||||
function getColorByValue(value, maxValue) {
|
||||
const ratio = value / maxValue;
|
||||
if (ratio > 0.8) return '#d73027';
|
||||
if (ratio > 0.6) return '#f46d43';
|
||||
if (ratio > 0.4) return '#fdae61';
|
||||
if (ratio > 0.2) return '#74add1';
|
||||
return '#4575b4';
|
||||
}
|
||||
|
||||
// Initialize everything when page loads
|
||||
window.addEventListener('load', function() {
|
||||
initMap();
|
||||
renderMarkers();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,402 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Radar Chart</title>
|
||||
|
||||
<script>
|
||||
{{CHART_MIN}}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8em;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 600px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#radarChart {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.stats-panel {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.85em;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.legend-panel {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.legend-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: #495057;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.legend-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<div class="chart-container">
|
||||
<canvas id="radarChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="stats-panel" id="statsPanel">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="dimensions">0</div>
|
||||
<div class="stat-label">Dimensions</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="datasets">0</div>
|
||||
<div class="stat-label">Datasets</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="maxScore">0</div>
|
||||
<div class="stat-label">Max Score</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="avgScore">0</div>
|
||||
<div class="stat-label">Average</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend-panel">
|
||||
<div class="legend-title">Dataset Legend</div>
|
||||
<div class="legend-items" id="legendItems">
|
||||
<!-- Legend items will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Data will be injected here
|
||||
const radarData = {{RADAR_DATA}};
|
||||
|
||||
let chart;
|
||||
|
||||
// Default color schemes for datasets
|
||||
const defaultColors = [
|
||||
'rgba(255, 99, 132, 1)', // Red
|
||||
'rgba(54, 162, 235, 1)', // Blue
|
||||
'rgba(75, 192, 192, 1)', // Teal
|
||||
'rgba(255, 159, 64, 1)', // Orange
|
||||
'rgba(153, 102, 255, 1)', // Purple
|
||||
'rgba(255, 206, 86, 1)', // Yellow
|
||||
'rgba(231, 76, 60, 1)', // Darker Red
|
||||
'rgba(46, 204, 113, 1)', // Green
|
||||
'rgba(52, 152, 219, 1)', // Light Blue
|
||||
'rgba(155, 89, 182, 1)', // Violet
|
||||
];
|
||||
|
||||
function getColor(index, alpha = 1) {
|
||||
const color = defaultColors[index % defaultColors.length];
|
||||
return alpha === 1 ? color : color.replace('1)', `${alpha})`);
|
||||
}
|
||||
|
||||
function processData(data) {
|
||||
// Ensure datasets have proper colors and styling
|
||||
const processedData = {
|
||||
labels: data.labels || [],
|
||||
datasets: (data.datasets || []).map((dataset, index) => {
|
||||
const borderColor = dataset.borderColor || getColor(index);
|
||||
const backgroundColor = dataset.backgroundColor || getColor(index, 0.2);
|
||||
|
||||
return {
|
||||
label: dataset.label || `Dataset ${index + 1}`,
|
||||
data: dataset.data || [],
|
||||
borderColor: borderColor,
|
||||
backgroundColor: backgroundColor,
|
||||
borderWidth: dataset.borderWidth || 2,
|
||||
pointBackgroundColor: dataset.pointBackgroundColor || borderColor,
|
||||
pointBorderColor: dataset.pointBorderColor || '#fff',
|
||||
pointHoverBackgroundColor: dataset.pointHoverBackgroundColor || '#fff',
|
||||
pointHoverBorderColor: dataset.pointHoverBorderColor || borderColor,
|
||||
fill: dataset.fill !== undefined ? dataset.fill : true,
|
||||
pointRadius: dataset.pointRadius !== undefined ? dataset.pointRadius : 4,
|
||||
pointHoverRadius: dataset.pointHoverRadius !== undefined ? dataset.pointHoverRadius : 6
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
return processedData;
|
||||
}
|
||||
|
||||
function initChart() {
|
||||
const ctx = document.getElementById('radarChart').getContext('2d');
|
||||
|
||||
const data = processData(radarData);
|
||||
|
||||
// Determine scale max from data
|
||||
let maxValue = 100;
|
||||
if (data.datasets.length > 0) {
|
||||
const allValues = data.datasets.flatMap(d => d.data);
|
||||
const dataMax = Math.max(...allValues);
|
||||
maxValue = Math.ceil(dataMax / 10) * 10; // Round up to nearest 10
|
||||
}
|
||||
|
||||
chart = new Chart(ctx, {
|
||||
type: 'radar',
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 15,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
titleColor: 'white',
|
||||
bodyColor: 'white',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
borderWidth: 1,
|
||||
cornerRadius: 6,
|
||||
displayColors: true,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const value = context.parsed.r;
|
||||
const percentage = ((value / maxValue) * 100).toFixed(1);
|
||||
return `${context.dataset.label}: ${value} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
r: {
|
||||
beginAtZero: true,
|
||||
max: maxValue,
|
||||
min: 0,
|
||||
ticks: {
|
||||
stepSize: maxValue / 5,
|
||||
font: {
|
||||
size: 10
|
||||
},
|
||||
color: '#666'
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
angleLines: {
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
pointLabels: {
|
||||
font: {
|
||||
size: 11,
|
||||
weight: 'bold'
|
||||
},
|
||||
color: '#333'
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
duration: 1000,
|
||||
easing: 'easeInOutQuart'
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'point'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateStats();
|
||||
updateLegend();
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const data = processData(radarData);
|
||||
const dimensions = data.labels.length;
|
||||
const datasets = data.datasets.length;
|
||||
|
||||
let allValues = [];
|
||||
data.datasets.forEach(dataset => {
|
||||
allValues = allValues.concat(dataset.data);
|
||||
});
|
||||
|
||||
const maxScore = allValues.length > 0 ? Math.max(...allValues) : 0;
|
||||
const avgScore = allValues.length > 0
|
||||
? allValues.reduce((a, b) => a + b, 0) / allValues.length
|
||||
: 0;
|
||||
|
||||
document.getElementById('dimensions').textContent = dimensions;
|
||||
document.getElementById('datasets').textContent = datasets;
|
||||
document.getElementById('maxScore').textContent = maxScore.toFixed(0);
|
||||
document.getElementById('avgScore').textContent = avgScore.toFixed(1);
|
||||
}
|
||||
|
||||
function updateLegend() {
|
||||
const legendContainer = document.getElementById('legendItems');
|
||||
const data = processData(radarData);
|
||||
|
||||
legendContainer.innerHTML = '';
|
||||
|
||||
data.datasets.forEach(dataset => {
|
||||
const legendItem = document.createElement('div');
|
||||
legendItem.className = 'legend-item';
|
||||
|
||||
const colorBox = document.createElement('div');
|
||||
colorBox.className = 'legend-color';
|
||||
colorBox.style.backgroundColor = dataset.borderColor;
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'legend-label';
|
||||
label.textContent = dataset.label;
|
||||
|
||||
legendItem.appendChild(colorBox);
|
||||
legendItem.appendChild(label);
|
||||
legendContainer.appendChild(legendItem);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to measure and report content size for iframe auto-resizing
|
||||
function reportContentSize() {
|
||||
// Get the actual content height
|
||||
const contentHeight = Math.max(
|
||||
document.body.scrollHeight,
|
||||
document.body.offsetHeight,
|
||||
document.documentElement.clientHeight,
|
||||
document.documentElement.scrollHeight,
|
||||
document.documentElement.offsetHeight
|
||||
);
|
||||
|
||||
// Send size change message to parent window (for MCP-UI iframe auto-resize)
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'ui-size-change',
|
||||
payload: {
|
||||
height: contentHeight
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and render on load
|
||||
window.onload = function() {
|
||||
initChart();
|
||||
|
||||
// Report initial size
|
||||
setTimeout(reportContentSize, 100);
|
||||
|
||||
// Watch for size changes using ResizeObserver if available
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
reportContentSize();
|
||||
});
|
||||
resizeObserver.observe(document.body);
|
||||
resizeObserver.observe(document.documentElement);
|
||||
}
|
||||
|
||||
// Fallback: also report on window resize
|
||||
window.addEventListener('resize', reportContentSize);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,283 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sankey Diagram</title>
|
||||
|
||||
<script>
|
||||
{{D3_MIN}}
|
||||
</script>
|
||||
<script>
|
||||
{{D3_SANKY}}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
#sankey {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.node rect {
|
||||
cursor: pointer;
|
||||
stroke: #000;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.node text {
|
||||
pointer-events: none;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.link {
|
||||
fill: none;
|
||||
stroke-opacity: 0.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
stroke-opacity: 0.8;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<svg id="sankey"></svg>
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Data will be injected here
|
||||
const sankeyData = {{SANKEY_DATA}};
|
||||
|
||||
// Sankey variables
|
||||
let svg, sankey, tooltip;
|
||||
const width = 500;
|
||||
const height = 400;
|
||||
const margin = { top: 20, right: 20, bottom: 20, left: 20 };
|
||||
|
||||
// Color schemes for different categories
|
||||
const colorSchemes = {
|
||||
source: "#ff6b6b",
|
||||
income: "#4ecdc4",
|
||||
input: "#45b7d1",
|
||||
generation: "#f9ca24",
|
||||
process: "#f0932b",
|
||||
pool: "#6c5ce7",
|
||||
distribution: "#a29bfe",
|
||||
logistics: "#fd79a8",
|
||||
landing: "#00b894",
|
||||
page: "#00cec9",
|
||||
storage: "#fdcb6e",
|
||||
retail: "#e17055",
|
||||
consumption: "#74b9ff",
|
||||
expense: "#fd79a8",
|
||||
conversion: "#00b894",
|
||||
end: "#00b894",
|
||||
savings: "#55a3ff",
|
||||
loss: "#636e72",
|
||||
exit: "#636e72",
|
||||
reverse: "#ddd",
|
||||
waste: "#636e72",
|
||||
default: "#69b3a2"
|
||||
};
|
||||
|
||||
// Initialize SVG
|
||||
function initializeSVG() {
|
||||
svg = d3.select("#sankey")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
svg.selectAll("*").remove();
|
||||
|
||||
sankey = d3.sankey()
|
||||
.nodeWidth(20)
|
||||
.nodePadding(10)
|
||||
.extent([[margin.left, margin.top], [width - margin.right, height - margin.bottom]]);
|
||||
|
||||
tooltip = d3.select("#tooltip");
|
||||
}
|
||||
|
||||
// Draw the Sankey diagram
|
||||
function drawSankey(data) {
|
||||
// Create a map of node names to indices
|
||||
const nodeMap = new Map();
|
||||
data.nodes.forEach((node, index) => {
|
||||
nodeMap.set(node.name, index);
|
||||
});
|
||||
|
||||
// Process data for D3 Sankey - convert node names to indices in links
|
||||
const processedLinks = data.links.map(link => ({
|
||||
source: nodeMap.get(link.source),
|
||||
target: nodeMap.get(link.target),
|
||||
value: link.value
|
||||
}));
|
||||
|
||||
const graph = sankey({
|
||||
nodes: data.nodes.map(d => ({ ...d })),
|
||||
links: processedLinks
|
||||
});
|
||||
|
||||
// Draw links
|
||||
svg.append("g")
|
||||
.selectAll("path")
|
||||
.data(graph.links)
|
||||
.enter().append("path")
|
||||
.attr("class", "link")
|
||||
.attr("d", d3.sankeyLinkHorizontal())
|
||||
.attr("stroke", d => {
|
||||
const sourceCategory = d.source.category;
|
||||
return colorSchemes[sourceCategory] || colorSchemes.default;
|
||||
})
|
||||
.attr("stroke-width", d => Math.max(1, d.width))
|
||||
.on("mouseover", function(event, d) {
|
||||
showLinkTooltip(event, d);
|
||||
})
|
||||
.on("mouseout", hideTooltip);
|
||||
|
||||
// Draw nodes
|
||||
const node = svg.append("g")
|
||||
.selectAll("g")
|
||||
.data(graph.nodes)
|
||||
.enter().append("g")
|
||||
.attr("class", "node");
|
||||
|
||||
node.append("rect")
|
||||
.attr("x", d => d.x0)
|
||||
.attr("y", d => d.y0)
|
||||
.attr("height", d => d.y1 - d.y0)
|
||||
.attr("width", d => d.x1 - d.x0)
|
||||
.attr("fill", d => colorSchemes[d.category] || colorSchemes.default)
|
||||
.on("mouseover", function(event, d) {
|
||||
showNodeTooltip(event, d);
|
||||
})
|
||||
.on("mouseout", hideTooltip);
|
||||
|
||||
// Add node labels
|
||||
node.append("text")
|
||||
.attr("x", d => d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6)
|
||||
.attr("y", d => (d.y1 + d.y0) / 2)
|
||||
.attr("dy", "0.35em")
|
||||
.attr("text-anchor", d => d.x0 < width / 2 ? "start" : "end")
|
||||
.text(d => d.name)
|
||||
.style("font-size", "12px")
|
||||
.style("fill", "#333");
|
||||
}
|
||||
|
||||
// Tooltip functions
|
||||
function showNodeTooltip(event, d) {
|
||||
const totalIn = d.targetLinks.reduce((sum, link) => sum + link.value, 0);
|
||||
const totalOut = d.sourceLinks.reduce((sum, link) => sum + link.value, 0);
|
||||
|
||||
tooltip.style("opacity", 1)
|
||||
.html(`
|
||||
<strong>${d.name}</strong><br/>
|
||||
Category: ${d.category || 'N/A'}<br/>
|
||||
Total In: ${totalIn.toLocaleString()}<br/>
|
||||
Total Out: ${totalOut.toLocaleString()}<br/>
|
||||
Net: ${(totalIn - totalOut).toLocaleString()}
|
||||
`)
|
||||
.style("left", (event.pageX + 10) + "px")
|
||||
.style("top", (event.pageY - 10) + "px");
|
||||
}
|
||||
|
||||
function showLinkTooltip(event, d) {
|
||||
tooltip.style("opacity", 1)
|
||||
.html(`
|
||||
<strong>Flow</strong><br/>
|
||||
From: ${d.source.name}<br/>
|
||||
To: ${d.target.name}<br/>
|
||||
Value: ${d.value.toLocaleString()}<br/>
|
||||
Width: ${d.width.toFixed(1)}px
|
||||
`)
|
||||
.style("left", (event.pageX + 10) + "px")
|
||||
.style("top", (event.pageY - 10) + "px");
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
tooltip.style("opacity", 0);
|
||||
}
|
||||
|
||||
// Function to measure and report content size for iframe auto-resizing
|
||||
function reportContentSize() {
|
||||
// Get the actual content height
|
||||
const contentHeight = Math.max(
|
||||
document.body.scrollHeight,
|
||||
document.body.offsetHeight,
|
||||
document.documentElement.clientHeight,
|
||||
document.documentElement.scrollHeight,
|
||||
document.documentElement.offsetHeight
|
||||
);
|
||||
|
||||
// Send size change message to parent window (for MCP-UI iframe auto-resize)
|
||||
// The message format must match what the MCP-UI client expects
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'ui-size-change', // lowercase as expected by MCP-UI client
|
||||
payload: {
|
||||
height: contentHeight
|
||||
// width is optional, we're not setting it to allow responsive design
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and render on load
|
||||
window.onload = function() {
|
||||
initializeSVG();
|
||||
drawSankey(sankeyData);
|
||||
|
||||
// Report initial size
|
||||
setTimeout(reportContentSize, 100);
|
||||
|
||||
// Watch for size changes using ResizeObserver if available
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
reportContentSize();
|
||||
});
|
||||
resizeObserver.observe(document.body);
|
||||
resizeObserver.observe(document.documentElement);
|
||||
}
|
||||
|
||||
// Fallback: also report on window resize
|
||||
window.addEventListener('resize', reportContentSize);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,321 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Treemap Visualization</title>
|
||||
|
||||
<script>
|
||||
{{D3_MIN}}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.treemap-container {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.treemap-rect {
|
||||
stroke: white;
|
||||
stroke-width: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.treemap-rect:hover {
|
||||
stroke-width: 3px;
|
||||
stroke: #333;
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.treemap-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
fill: white;
|
||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.7);
|
||||
pointer-events: none;
|
||||
text-anchor: middle;
|
||||
dominant-baseline: middle;
|
||||
}
|
||||
|
||||
.treemap-value {
|
||||
font-size: 10px;
|
||||
fill: rgba(255,255,255,0.8);
|
||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.7);
|
||||
pointer-events: none;
|
||||
text-anchor: middle;
|
||||
dominant-baseline: middle;
|
||||
}
|
||||
|
||||
.group-label {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
fill: #333;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0,0,0,0.9);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="treemap-container" id="treemap"></div>
|
||||
</div>
|
||||
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
|
||||
<script>
|
||||
// Data will be injected here
|
||||
const treemapData = {{TREEMAP_DATA}};
|
||||
|
||||
let currentData = treemapData;
|
||||
let currentRoot = treemapData;
|
||||
let svg, treemap, root;
|
||||
|
||||
const width = 800;
|
||||
const height = 500;
|
||||
|
||||
// Color scheme for different categories
|
||||
const colorScale = d3.scaleOrdinal(d3.schemeTableau10);
|
||||
|
||||
function initializeTreemap() {
|
||||
const container = d3.select("#treemap");
|
||||
container.selectAll("*").remove();
|
||||
|
||||
svg = container.append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.style("font", "10px sans-serif");
|
||||
|
||||
treemap = d3.treemap()
|
||||
.size([width, height])
|
||||
.padding(2)
|
||||
.round(true);
|
||||
}
|
||||
|
||||
function updateTreemap(data, isRoot = true) {
|
||||
currentData = data;
|
||||
if (isRoot) {
|
||||
currentRoot = data;
|
||||
}
|
||||
|
||||
// Create hierarchy and calculate layout
|
||||
root = d3.hierarchy(data)
|
||||
.sum(d => d.value || 0)
|
||||
.sort((a, b) => b.value - a.value);
|
||||
|
||||
treemap(root);
|
||||
|
||||
// Clear previous content
|
||||
svg.selectAll("*").remove();
|
||||
|
||||
// Create leaf nodes
|
||||
const leaf = svg.selectAll("g")
|
||||
.data(root.leaves())
|
||||
.join("g")
|
||||
.attr("transform", d => `translate(${d.x0},${d.y0})`);
|
||||
|
||||
// Add rectangles
|
||||
leaf.append("rect")
|
||||
.attr("class", "treemap-rect")
|
||||
.attr("fill", d => {
|
||||
// Use category for color, or parent name if no category
|
||||
const colorKey = d.data.category || d.parent.data.name || 'default';
|
||||
return colorScale(colorKey);
|
||||
})
|
||||
.attr("width", d => d.x1 - d.x0)
|
||||
.attr("height", d => d.y1 - d.y0)
|
||||
.on("mouseover", function(event, d) {
|
||||
showTooltip(event, d);
|
||||
})
|
||||
.on("mousemove", function(event, d) {
|
||||
moveTooltip(event);
|
||||
})
|
||||
.on("mouseout", function() {
|
||||
hideTooltip();
|
||||
})
|
||||
.on("click", function(event, d) {
|
||||
// Navigate to parent if it has children
|
||||
if (d.parent && d.parent.data.children) {
|
||||
updateTreemap(d.parent.data, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Add text labels
|
||||
leaf.append("text")
|
||||
.attr("class", "treemap-text")
|
||||
.attr("x", d => (d.x1 - d.x0) / 2)
|
||||
.attr("y", d => (d.y1 - d.y0) / 2 - 5)
|
||||
.text(d => d.data.name)
|
||||
.style("font-size", d => Math.min((d.x1 - d.x0) / 8, (d.y1 - d.y0) / 4, 14) + "px")
|
||||
.each(function(d) {
|
||||
const textLength = this.getComputedTextLength();
|
||||
const availableWidth = d.x1 - d.x0 - 4;
|
||||
if (textLength > availableWidth) {
|
||||
const text = d.data.name;
|
||||
const ratio = availableWidth / textLength;
|
||||
const maxLength = Math.floor(text.length * ratio) - 3;
|
||||
if (maxLength > 0) {
|
||||
d3.select(this).text(text.substring(0, maxLength) + "...");
|
||||
} else {
|
||||
d3.select(this).text("");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add value labels
|
||||
leaf.append("text")
|
||||
.attr("class", "treemap-value")
|
||||
.attr("x", d => (d.x1 - d.x0) / 2)
|
||||
.attr("y", d => (d.y1 - d.y0) / 2 + 10)
|
||||
.text(d => formatValue(d.data.value))
|
||||
.style("font-size", d => Math.min((d.x1 - d.x0) / 10, (d.y1 - d.y0) / 6, 12) + "px")
|
||||
.style("display", d => {
|
||||
// Hide value if rectangle is too small
|
||||
const rectHeight = d.y1 - d.y0;
|
||||
return rectHeight < 30 ? "none" : "block";
|
||||
});
|
||||
|
||||
// Add group boundaries and labels for navigation (if data has children)
|
||||
if (data.children) {
|
||||
// Add group rectangles for visual hierarchy
|
||||
const groups = svg.selectAll(".group-rect")
|
||||
.data(root.children || [])
|
||||
.join("rect")
|
||||
.attr("class", "group-rect")
|
||||
.attr("x", d => d.x0)
|
||||
.attr("y", d => d.y0)
|
||||
.attr("width", d => d.x1 - d.x0)
|
||||
.attr("height", d => d.y1 - d.y0)
|
||||
.attr("fill", "none")
|
||||
.attr("stroke", "#333")
|
||||
.attr("stroke-width", 2)
|
||||
.attr("stroke-dasharray", "3,3")
|
||||
.style("cursor", "pointer")
|
||||
.style("pointer-events", "none");
|
||||
|
||||
// Add group labels
|
||||
svg.selectAll(".group-label")
|
||||
.data(root.children || [])
|
||||
.join("text")
|
||||
.attr("class", "group-label")
|
||||
.attr("x", d => d.x0 + 5)
|
||||
.attr("y", d => d.y0 + 15)
|
||||
.text(d => d.data.name)
|
||||
.style("display", d => {
|
||||
// Hide label if group is too small
|
||||
const groupWidth = d.x1 - d.x0;
|
||||
return groupWidth < 50 ? "none" : "block";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showTooltip(event, d) {
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
const percentage = ((d.data.value / root.value) * 100).toFixed(1);
|
||||
|
||||
tooltip.innerHTML = `
|
||||
<strong>${d.data.name}</strong><br>
|
||||
Value: ${formatValue(d.data.value)}<br>
|
||||
${d.data.category ? `Category: ${d.data.category}<br>` : ''}
|
||||
Percentage: ${percentage}%
|
||||
${d.parent ? `<br>Parent: ${d.parent.data.name}` : ''}
|
||||
`;
|
||||
tooltip.style.opacity = 1;
|
||||
moveTooltip(event);
|
||||
}
|
||||
|
||||
function moveTooltip(event) {
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
tooltip.style.left = (event.pageX + 10) + 'px';
|
||||
tooltip.style.top = (event.pageY - 10) + 'px';
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
tooltip.style.opacity = 0;
|
||||
}
|
||||
|
||||
function formatValue(value) {
|
||||
if (value >= 1000000) {
|
||||
return (value / 1000000).toFixed(1) + 'M';
|
||||
} else if (value >= 1000) {
|
||||
return (value / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return value.toLocaleString();
|
||||
}
|
||||
|
||||
// Function to measure and report content size for iframe auto-resizing
|
||||
function reportContentSize() {
|
||||
// Get the actual content height
|
||||
const contentHeight = Math.max(
|
||||
document.body.scrollHeight,
|
||||
document.body.offsetHeight,
|
||||
document.documentElement.clientHeight,
|
||||
document.documentElement.scrollHeight,
|
||||
document.documentElement.offsetHeight
|
||||
);
|
||||
|
||||
// Send size change message to parent window (for MCP-UI iframe auto-resize)
|
||||
// The message format must match what the MCP-UI client expects
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'ui-size-change', // lowercase as expected by MCP-UI client
|
||||
payload: {
|
||||
height: contentHeight
|
||||
// width is optional, we're not setting it to allow responsive design
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and render on load
|
||||
window.onload = function() {
|
||||
initializeTreemap();
|
||||
updateTreemap(treemapData);
|
||||
|
||||
// Report initial size
|
||||
setTimeout(reportContentSize, 100);
|
||||
|
||||
// Watch for size changes using ResizeObserver if available
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
reportContentSize();
|
||||
});
|
||||
resizeObserver.observe(document.body);
|
||||
resizeObserver.observe(document.documentElement);
|
||||
}
|
||||
|
||||
// Fallback: also report on window resize
|
||||
window.addEventListener('resize', reportContentSize);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,11 +7,13 @@ pub static APP_STRATEGY: Lazy<AppStrategyArgs> = Lazy::new(|| AppStrategyArgs {
|
||||
app_name: "goose".to_string(),
|
||||
});
|
||||
|
||||
pub mod autovisualiser;
|
||||
pub mod computercontroller;
|
||||
mod developer;
|
||||
mod memory;
|
||||
mod tutorial;
|
||||
|
||||
pub use autovisualiser::AutoVisualiserRouter;
|
||||
pub use computercontroller::ComputerControllerRouter;
|
||||
pub use developer::DeveloperRouter;
|
||||
pub use memory::MemoryRouter;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use goose_mcp::{ComputerControllerRouter, DeveloperRouter, MemoryRouter, TutorialRouter};
|
||||
use goose_mcp::{
|
||||
AutoVisualiserRouter, ComputerControllerRouter, DeveloperRouter, MemoryRouter, TutorialRouter,
|
||||
};
|
||||
use mcp_server::router::RouterService;
|
||||
use mcp_server::{BoundedService, ByteTransport, Server};
|
||||
use tokio::io::{stdin, stdout};
|
||||
@@ -17,6 +19,7 @@ pub async fn run(name: &str) -> Result<()> {
|
||||
let router: Option<Box<dyn BoundedService>> = match name {
|
||||
"developer" => Some(Box::new(RouterService(DeveloperRouter::new()))),
|
||||
"computercontroller" => Some(Box::new(RouterService(ComputerControllerRouter::new()))),
|
||||
"autovisualiser" => Some(Box::new(RouterService(AutoVisualiserRouter::new()))),
|
||||
"memory" => Some(Box::new(RouterService(MemoryRouter::new()))),
|
||||
"tutorial" => Some(Box::new(RouterService(TutorialRouter::new()))),
|
||||
_ => None,
|
||||
|
||||
@@ -17,6 +17,15 @@
|
||||
"env_keys": [],
|
||||
"timeout": 300
|
||||
},
|
||||
{
|
||||
"id": "autovisualiser",
|
||||
"name": "Auto Visualiser",
|
||||
"description": "Automatically see your data with this UI generation extension.",
|
||||
"enabled": false,
|
||||
"type": "builtin",
|
||||
"env_keys": [],
|
||||
"timeout": 300
|
||||
},
|
||||
{
|
||||
"id": "memory",
|
||||
"name": "Memory",
|
||||
|
||||
@@ -21,6 +21,17 @@
|
||||
"timeout": 300,
|
||||
"bundled": true
|
||||
},
|
||||
{
|
||||
"id": "autovisualiser",
|
||||
"name": "autovisualiser",
|
||||
"display_name": "Auto Visualiser",
|
||||
"description": "Data visualization and UI generation tools",
|
||||
"enabled": false,
|
||||
"type": "builtin",
|
||||
"env_keys": [],
|
||||
"timeout": 300,
|
||||
"bundled": true
|
||||
},
|
||||
{
|
||||
"id": "memory",
|
||||
"name": "memory",
|
||||
|
||||
Reference in New Issue
Block a user