Move heatmap visualization to new script endpoint

This commit is contained in:
guerler
2024-11-25 21:52:04 +03:00
parent 7a68911051
commit 77d335ecff
10 changed files with 38 additions and 610 deletions
+1 -2
View File
@@ -21,7 +21,6 @@ const STATIC_PLUGIN_BUILD_IDS = [
"fits_graph_viewer",
"fits_image_viewer",
"h5web",
"heatmap/heatmap_default",
"hyphyvision",
"jqplot/jqplot_bar",
"media_player",
@@ -35,7 +34,7 @@ const STATIC_PLUGIN_BUILD_IDS = [
"tiffviewer",
"ts_visjs",
];
const INSTALL_PLUGIN_BUILD_IDS = ["cytoscape", "ngl", "msa", "openlayers", "phylocanvas", "venn", "vizarr"]; // todo: derive from XML
const INSTALL_PLUGIN_BUILD_IDS = ["cytoscape", "heatmap", "ngl", "msa", "openlayers", "phylocanvas", "venn", "vizarr"]; // todo: derive from XML
const DIST_PLUGIN_BUILD_IDS = ["new_user"];
const PLUGIN_BUILD_IDS = Array.prototype.concat(DIST_PLUGIN_BUILD_IDS, STATIC_PLUGIN_BUILD_IDS);
@@ -1,5 +1,10 @@
<macros>
<xml name="shared">
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE visualization SYSTEM "../../visualization.dtd">
<visualization name="Heatmap" embeddable="false">
<description>Renders a heatmap from matrix data provided in 3-column format (x, y, observation).</description>
<requirements>
<requirement type="npm" version="0.0.4" package="@galaxyproject/heatmap"/>
</requirements>
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
@@ -11,7 +16,8 @@
<params>
<param type="dataset" var_name_in_template="hda" required="true">dataset_id</param>
</params>
<groups>
<entry_point entry_point_type="script" src="dist/index.js" css="dist/index.css" />
<tracks>
<input>
<label>Provide a label</label>
<name>key</name>
@@ -23,23 +29,19 @@
<label>Column labels</label>
<name>x</name>
<type>data_column</type>
<is_label>true</is_label>
<is_numeric>true</is_numeric>
</input>
<input>
<label>Row labels</label>
<name>y</name>
<type>data_column</type>
<is_label>true</is_label>
<is_numeric>true</is_numeric>
</input>
<input>
<label>Observation</label>
<name>z</name>
<type>data_column</type>
<is_numeric>true</is_numeric>
<is_number>true</is_number>
</input>
</groups>
</tracks>
<settings>
<input>
<name>color_set</name>
@@ -480,5 +482,11 @@
<value>true</value>
</input>
</settings>
</xml>
</macros>
<specs>
<exports>
<exports>pdf</exports>
<exports>png</exports>
<exports>svg</exports>
</exports>
</specs>
</visualization>
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE visualization SYSTEM "../../visualization.dtd">
<visualization name="Clustered Heatmap" embeddable="false">
<macros>
<import>../../heatmap_default/config/heatmap_shared.xml</import>
</macros>
<description>Applies hierarchical clustering to a matrix using R. The data has to be provided in 3-column format. The result is displayed as clustered heatmap.</description>
<entry_point entry_point_type="chart" src="../../heatmap_default/static/dist/script.js" load="heatmap_cluster"/>
<expand macro="shared"/>
<specs>
<zoomable>true</zoomable>
<confirm>True</confirm>
<exports>
<exports>pdf</exports>
<exports>png</exports>
<exports>svg</exports>
</exports>
</specs>
</visualization>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE visualization SYSTEM "../../visualization.dtd">
<visualization name="Heatmap" embeddable="false">
<macros>
<import>heatmap_shared.xml</import>
</macros>
<description>Renders a heatmap from matrix data provided in 3-column format (x, y, observation).</description>
<entry_point entry_point_type="chart" src="dist/script.js" load="heatmap_default"/>
<expand macro="shared"/>
<specs>
<exports>
<exports>pdf</exports>
<exports>png</exports>
<exports>svg</exports>
</exports>
</specs>
</visualization>
@@ -1,24 +0,0 @@
{
"name": "visualization",
"version": "0.2.0",
"keywords": [
"galaxy",
"visualization"
],
"license": "AFL-3.0",
"dependencies": {
"@galaxyproject/charts": "^0.1.0",
"babel-preset-env": "^1.6.1",
"backbone": "^1.4.0",
"bootstrap": "^3.3.7",
"d3": "3",
"jquery": "^3.5.1",
"underscore": "^1.11.0"
},
"scripts": {
"build": "parcel build src/script.js --dist-dir static/dist"
},
"devDependencies": {
"parcel": "2.11.0"
}
}
@@ -1,31 +0,0 @@
export default {
seis: ['#AA0000', '#D00000', '#F70000', '#FF1D00', '#FF4400', '#FF6A00', '#FF9000', '#FFB700', '#FFDD00', '#FFFF00', '#FFFF00', '#FFFF00', '#BDFF0C', '#73FF1A', '#3FFA36', '#16F45A', '#00D08B', '#0087CD', '#0048FA', '#0024E3'].reverse(),
sealand: ['#8C66FF', '#6A66FF', '#6684FF', '#66A7FF', '#66CAFF', '#66ECFF', '#66FFF0', '#66FFCE', '#66FFAB', '#66FF88', '#66FF66', '#88FF66', '#ABFF66', '#CEFF66', '#FFEEA6', '#FFD3A6', '#FFB8A6', '#FFAAB0', '#FFB5CB', '#FFC0E1'].reverse(),
redgreen: ['#005824', '#1A693B', '#347B53', '#4F8D6B', '#699F83', '#83B09B', '#9EC2B3', '#B8D4CB', '#D2E6E3', '#EDF8FB', '#FFFFFF', '#F1EEF6', '#E6D3E1','#DBB9CD', '#D19EB9', '#C684A4', '#BB6990', '#B14F7C', '#A63467', '#9B1A53', '#91003F'],
ocean: ['#000000', '#000209', '#000413', '#00061E', '#000728', '#000932', '#002650', '#00426E', '#005E8C', '#007AAA', '#0096C8', '#22A9C2', '#45BCBB', '#67CFB5', '#8AE2AE', '#ACF6A8', '#BCF8B9', '#CBF9CA', '#DBFBDC', '#EBFDED'].reverse(),
cool: ['#00FFFF', '#0DF2FF', '#19E6FF', '#26D9FF', '#33CCFF', '#3FBFFF', '#4CB3FF', '#59A6FF', '#6699FF', '#738CFF', '#7F7FFF', '#8C73FF', '#9966FF', '#A659FF', '#B24DFF', '#BF3FFF', '#CC33FF', '#D926FF', '#E619FF', '#F20DFF'],
copper: ['#000000', '#100906', '#1F130D', '#301E13', '#40281A', '#50321F', '#603C26', '#70462D', '#805033', '#905A3A', '#A06440', '#B06E46', '#C0784D', '#D08253', '#E08C5A', '#F09660', '#FFA066', '#FFAA6D', '#FFB473', '#FFBE7A'].reverse(),
gray: ['#000000', '#0D0D0D', '#191919', '#262626', '#333333', '#3F3F3F', '#4C4C4C', '#595959', '#666666', '#737373', '#7F7F7F', '#8C8C8C', '#999999', '#A6A6A6', '#B2B2B2', '#BFBFBF', '#CCCCCC', '#D9D9D9', '#E6E6E6', '#F2F2F2'].reverse(),
hot: ['#000000', '#220000', '#440000', '#660000', '#880000', '#AA0000', '#CC0000', '#EE0000', '#FF1100', '#FF3300', '#FF5500', '#FF7700', '#FF9900', '#FFBB00', '#FFDD00', '#FFFF00', '#FFFF33', '#FFFF66', '#FFFF99', '#FFFFCC'].reverse(),
jet: ['#00007F', '#0000B2', '#0000E5', '#0019FF', '#004DFF', '#007FFF', '#00B2FF', '#00E5FF', '#FFFFF2', '#FFFFD9', '#FFFFBF', '#FFFFA5', '#FFFF8C', '#FFE500', '#FFB300', '#FF7F00', '#FF4C00', '#FF1900', '#E50000', '#B20000'],
no_green: ['#1F60FF', '#1F60FF', '#1F9FFF', '#1FBFFF', '#00CFFF', '#2AFFFF', '#2AFFFF', '#55FFFF', '#7FFFFF', '#AAFFFF', '#FFFF54', '#FFFF54', '#FFF000', '#FFBF00', '#FFA800', '#FF8A00', '#FF8A00', '#FF7000', '#FF4D00', '#FF0000'],
polar: ['#0000FF', '#1919FF', '#3333FF', '#4C4CFF', '#6666FF', '#7F7FFF', '#9999FF', '#B2B2FF', '#CCCCFF', '#E6E6FF', '#FFFFFF', '#FFE5E5', '#FFCCCC', '#FFB2B2', '#FF9999', '#FF7F7F', '#FF6666', '#FF4C4C', '#FF3333', '#FF1A1A'],
red2green: ['#FF0000', '#FF1919', '#FF3333', '#FF4C4C', '#FF6666', '#FF7F7F', '#FF9999', '#FFB2B2', '#FFCCCC', '#FFE6E6', '#FFFFFF', '#E5FFE5', '#CCFFCC', '#B2FFB2', '#99FF99', '#7FFF7F', '#66FF66', '#4CFF4C', '#33FF33', '#1AFF1A'].reverse(),
relief: ['#000000', '#000413', '#000728', '#002650', '#005E8C', '#0096C8', '#45BCBB', '#8AE2AE', '#BCF8B9', '#DBFBDC', '#467832', '#887438', '#B19D48', '#DBC758', '#FAE769', '#FAEB7E', '#FCED93', '#FCF1A7', '#FCF6C1', '#FDFAE0'].reverse(),
split: ['#7F7FFF', '#6666E6', '#4D4DCC', '#3333B3', '#1A1A99', '#00007F', '#000066', '#00004D', '#000033', '#00001A', '#000000', '#1A0000', '#330000', '#4D0000', '#660000', '#7F0000', '#991A1A', '#B33333', '#CC4D4D', '#E66666'],
wysiwyg: ['#3F003F', '#3F003F', '#3F00BF', '#003FFF', '#00A0FF', '#3FBFFF', '#3FBFFF', '#40E0FF', '#3FFFBF', '#3FFF3F', '#7FFF3F', '#BFFF3F', '#BFFF3F', '#FFE040', '#FFE040', '#FF6040', '#FF1F40', '#FF60C0', '#FFA0FF', '#FFA0FF'].reverse()
}
@@ -1,506 +0,0 @@
import _ from "underscore";
import $ from "jquery";
import * as d3 from "d3";
import Backbone from "backbone";
import Colors from "./colorsets";
import { addZoom, makeUniqueCategories, makeTickFormat } from "@galaxyproject/charts/lib/utilities/series";
import { requestPanels } from "@galaxyproject/charts/lib/utilities/datasets";
import { requestCharts, request as requestJobs } from "@galaxyproject/charts/lib/utilities/jobs";
var CommonWrapper = Backbone.View.extend({
optionsDefault: {
margin: {
top: 40,
right: 70,
bottom: 70,
left: 70,
},
style: {
"font-weight": "normal",
"font-family": "Verdana",
"font-size": 12,
},
legend: {
width: 15,
size: 0.9,
style: {
"font-weight": "normal",
"font-family": "Verdana",
"font-size": 11,
},
limit: 7,
},
background_color: "#FFFFFF",
debug_color: "#FFFFFF",
},
initialize: function (options) {
var self = this;
this.chart = options.chart;
this.canvas_id = options.canvas_id;
this.group = options.groups[0];
this.data = options.groups[0].values;
this.options = _.defaults(this.optionsDefault, options);
// get color set
this.color_set = Colors[this.chart.settings.get("color_set", "seism")];
// categories
this.categories = makeUniqueCategories([this.group]);
// domains/scales
this.xScale = d3.scale.linear().domain([0, this.categories.array.x.length]);
this.yScale = d3.scale.linear().domain([0, this.categories.array.y.length]);
// color scale
this.zMin = d3.min(this.data, function (d) {
return d.z;
});
this.zMax = d3.max(this.data, function (d) {
return d.z;
});
this.zScale = d3.scale.quantize().domain([this.zMin, this.zMax]).range(this.color_set);
// create axis
this.xAxis = d3.svg.axis().scale(this.xScale).orient("bottom");
this.yAxis = d3.svg.axis().scale(this.yScale).orient("left");
// make categories
this._makeTickFormat("x");
this._makeTickFormat("y");
// add tooltip
this.tooltip = d3
.select(".charts-viewport-container")
.append("div")
.attr("class", "charts-tooltip")
.style(this.options.style)
.style("opacity", 0);
// refresh on window resize
$(window).on("resize", function () {
self.redraw();
});
this.redraw();
addZoom({
xAxis: this.xAxis,
yAxis: this.yAxis,
redraw: function () {
self.redraw();
},
svg: d3.select("#" + this.canvas_id),
});
},
/** Redraw */
redraw: function () {
// get/reset container
var container = $("#" + this.canvas_id);
container.empty();
// get domain
var xDomain = this.xScale.domain();
var yDomain = this.yScale.domain();
// set ticks
var xTickStart = Math.ceil(xDomain[0]);
var xTickEnd = Math.floor(xDomain[1]);
var yTickStart = Math.ceil(yDomain[0]);
var yTickEnd = Math.floor(yDomain[1]);
this.xAxis.tickValues(d3.range(xTickStart, xTickEnd, 1));
this.yAxis.tickValues(d3.range(yTickStart, yTickEnd, 1));
// get margins
var margin = this.options.margin;
// configure dimensions
this.height = parseInt(container.height()) - margin.top - margin.bottom;
this.width = parseInt(container.width()) - margin.left - margin.right;
// set range
this.xScale.range([0, this.width]);
this.yScale.range([this.height, 0]);
// get box properties
this.rowCount = yDomain[1] - yDomain[0];
this.colCount = xDomain[1] - xDomain[0];
this.boxWidth = Math.max(1, Math.floor(this.width / this.colCount));
this.boxHeight = Math.max(1, Math.floor(this.height / this.rowCount));
// create group
this.svg = d3
.select("#" + this.canvas_id)
.append("g")
.attr("class", "heatmap")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// build elements
this._buildBoxes();
this._buildX();
this._buildY();
// show legend only if requested
if (String(this.chart.settings.get("show_legend")).toLocaleLowerCase() == "true") {
this._buildLegend();
}
},
/** Build boxes */
_buildBoxes: function () {
var self = this;
var height = this.height;
var width = this.width;
var svg = this.svg;
var boxWidth = this.boxWidth;
var boxHeight = this.boxHeight;
function _locator(d) {
return "translate(" + self.xScale(d.x) + "," + self.yScale(d.y + 1) + ")";
}
function _color(d) {
return self.zScale(d.z);
}
// set background color
svg.append("rect").attr("width", width).attr("height", height).attr("fill", this.options.background_color);
// clip path
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
// create chart area
var chartBody = svg.append("g").attr("clip-path", "url(#clip)");
// add boxes to chart area
var boxes = chartBody.selectAll("box-group").data(this.data, function (d, i) {
return d.x + "\0" + d.y;
});
var gEnter = boxes.enter().append("g").attr("class", "box-group");
gEnter.append("rect").attr("class", "heat-box");
boxes
.selectAll("rect")
.attr("rx", 1)
.attr("ry", 1)
.attr("fill", _color)
.attr("width", boxWidth)
.attr("height", boxHeight)
.attr("transform", _locator);
// add tooltip events
boxes
.selectAll("rect")
.on("dblclick", function (d) {
var url = self.chart.settings.get("url_template").trim();
if (url) {
d3.event.stopPropagation();
var xLabel = self.categories.array.x[d.x];
var yLabel = self.categories.array.y[d.y];
window.open(url.replace("__LABEL__", xLabel));
window.open(url.replace("__LABEL__", yLabel));
}
})
.on("mouseover", function (d) {
var matrix = this.getScreenCTM().translate(+this.getAttribute("cx"), +this.getAttribute("cy"));
self.tooltip.style("opacity", 0.9);
self.tooltip
.html(self._templateTooltip(d))
.style("left", window.pageXOffset + matrix.e + 15 + "px")
.style("top", window.pageYOffset + matrix.f - 30 + "px");
})
.on("mouseout", function (d) {
self.tooltip.style("opacity", 0);
});
// initially hide tooltips
this.tooltip.style("opacity", 0);
// exit
boxes.exit().remove();
},
/** Build x axis */
_buildX: function () {
var height = this.height;
var width = this.width;
var margin = this.options.margin;
var svg = this.svg;
var font_size = this.options.style["font-size"];
var boxWidth = this.boxWidth;
// draw x axis
this.gxAxis = svg
.append("g")
.attr("class", "x axis")
.style("stroke-width", 1)
.attr("transform", "translate(0," + height + ")")
.call(this.xAxis);
// fix text
var xFontSize = Math.min(boxWidth, font_size);
this.gxAxis
.selectAll("text")
.style(this.options.style)
.style({ "font-size": xFontSize + "px" })
.attr("transform", function (d) {
var y = -this.getBBox().height - 15;
var x = -xFontSize + boxWidth / 2;
return "rotate(-90)translate(" + y + "," + x + ")";
});
// set background color
svg.append("rect")
.attr("width", width)
.attr("height", font_size + 3)
.attr("y", height + margin.bottom - font_size - 3)
.attr("fill", this.options.debug_color)
.attr("opacity", 0.7);
// axis label
this.gxAxisLabel = svg
.append("text")
.attr("class", "x label")
.style(this.options.style)
.text(this.chart.settings.get("x_axis_label"))
.attr("transform", function (d) {
var y = height + margin.bottom - font_size / 3;
var x = (width - this.getBBox().width) / 2;
return "translate(" + x + "," + y + ")";
});
// chart title
this.gxTickLabel = svg
.append("text")
.attr("class", "title")
.style(this.options.style)
.style({ "font-size": 1.1 * font_size })
.text(this.group.key)
.attr("transform", function (d) {
var y = -margin.top / 2;
var x = (width - this.getBBox().width) / 2;
return "translate(" + x + "," + y + ")";
});
},
/** Build y axis */
_buildY: function () {
var height = this.height;
var margin = this.options.margin;
var svg = this.svg;
var font_size = this.options.style["font-size"];
var boxHeight = this.boxHeight;
// draw y axis
this.gyAxis = svg.append("g").attr("class", "y axis").style("stroke-width", 1).call(this.yAxis);
// fix text
var yFontSize = Math.min(boxHeight, font_size);
this.gyAxis
.selectAll("text")
.style(this.options.style)
.style({ "font-size": yFontSize + "px" })
.attr("y", -boxHeight / 2);
// set background color
svg.append("rect")
.attr("width", font_size)
.attr("height", height)
.attr("x", -margin.left)
.attr("fill", this.options.debug_color)
.attr("opacity", 0.7);
// axis label
this.gyAxisLabel = svg
.append("text")
.attr("class", "y label")
.style(this.options.style)
.text(this.chart.settings.get("y_axis_label"))
.attr("transform", function (d) {
var x = -margin.left + font_size - 2;
var y = -(height + this.getBBox().width) / 2;
return "rotate(-90)translate(" + y + "," + x + ")";
});
},
/** Build legend */
_buildLegend: function () {
var self = this;
var height = this.height;
var width = this.width;
var margin = this.options.margin;
var font_size = this.options.legend.style["font-size"];
var limit = this.options.legend.limit;
var legendSize = this.options.legend.size;
var legendWidth = this.options.legend.width;
var legendElements = this.zScale.range().length;
var legendElementHeight = Math.max((legendSize * height) / legendElements, font_size);
var legendHeight = (legendElements * legendElementHeight) / 2;
var data = d3.range(this.zMin, this.zMax, (2 * (this.zMax - this.zMin)) / legendElements).reverse();
if (data.length < 2) {
return;
}
var legend = this.svg
.selectAll(".legend")
.data(data)
.enter()
.append("g")
.attr("class", "legend")
.attr("transform", function (d, i) {
var x = width + 10;
var y = (height - legendHeight) / 2 + i * legendElementHeight;
return "translate(" + x + "," + y + ")";
});
legend
.append("rect")
.attr("width", legendWidth)
.attr("height", legendElementHeight)
.style("fill", function (z) {
return self.zScale(z);
});
legend
.append("text")
.attr("x", legendWidth + 4)
.attr("y", function () {
return (legendElementHeight + this.getBBox().height) / 2;
})
.style(this.options.legend.style)
.text(function (d) {
return String(d).length > limit ? String(d).substr(0, limit - 2) + ".." : String(d);
});
this.svg
.append("text")
.style(this.options.legend.style)
.style({ "font-size": 9, "font-weight": "bold" })
.text("Legend")
.attr("transform", function (d, i) {
var x = width + (margin.right - this.getBBox().width) / 2;
var y = (height - legendHeight) / 2 - 10;
return "translate(" + x + "," + y + ")";
});
},
/** Create axes formatting */
_makeTickFormat: function (id) {
var settings = this.chart.settings;
var self = this;
makeTickFormat({
categories: self.categories.array[id],
type: settings.get(id + "_axis_type|type"),
precision: settings.get(id + "_axis_type|precision"),
formatter: function (formatter) {
if (formatter) {
self[id + "Axis"].tickFormat(function (value) {
return formatter(value);
});
}
},
});
},
/** Handle error */
_handleError: function (err) {
this.chart.state("failed", err);
},
/** Main template */
_templateTooltip: function (d) {
var x = this.categories.array.x[d.x];
var y = this.categories.array.y[d.y];
var z = d.z;
return (
"<table>" +
"<tr>" +
'<td class="charts-tooltip-first">Row:</td>' +
"<td>" +
y +
"</td>" +
"</tr>" +
"<tr>" +
'<td class="charts-tooltip-first">Column:</td>' +
"<td>" +
x +
"</td>" +
"</tr>" +
"<tr>" +
'<td class="charts-tooltip-first">Value:</td>' +
"<td>" +
z +
"</td>" +
"</tr>" +
"</table>"
);
},
});
/* Prepare containers */
function createContainers(tag, chart, target) {
var n = chart.groups.length;
var $container = $("#" + target);
$container.empty();
const targets = [];
for (var i = 0; i < n; i++) {
var panel_id = "vis-container-id-" + i;
var $panel = $("<" + tag + " style='float: left; height: 100%;' />").attr("id", panel_id);
$panel.width(parseInt(100 / n) + "%");
$container.append($panel);
targets.push(panel_id);
}
return targets;
}
window.bundleEntries = window.bundleEntries || {};
window.bundleEntries.heatmap_default = function (options) {
options.targets = createContainers("svg", options.chart, options.target);
options.render = function (canvas_id, groups) {
new CommonWrapper({
chart: options.chart,
canvas_id: canvas_id,
groups: groups,
});
return true;
};
requestPanels(options);
}
window.bundleEntries.heatmap_cluster = function (options) {
options.targets = createContainers("svg", options.chart, options.target);
requestJobs(
options.chart,
requestCharts(options.chart, "heatmap"),
function (dataset) {
var dataset_groups = new Backbone.Collection();
options.chart.groups.each(function (group, index) {
dataset_groups.add({
__data_columns: {
x: { is_label: true },
y: { is_label: true },
z: { is_numeric: true },
},
x: index++,
y: index++,
z: index++,
key: group.get("key"),
});
});
options.dataset_id = dataset.id;
options.dataset_groups = dataset_groups;
options.render = function (canvas_id, groups) {
new CommonWrapper({
chart: options.chart,
canvas_id: canvas_id,
groups: groups,
});
return true;
};
requestPanels(options);
},
function () {
options.process.reject();
}
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,18 @@
<svg width="200" height="200" viewBox="0 0 5 5" xmlns="http://www.w3.org/2000/svg">
<rect x="0.0" y="0.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="1.0" y="0.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="2.0" y="0.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="3.0" y="0.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="0.0" y="1.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="1.0" y="1.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="2.0" y="1.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="3.0" y="1.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="0.0" y="2.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="1.0" y="2.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="2.0" y="2.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="3.0" y="2.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="0.0" y="3.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="1.0" y="3.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="2.0" y="3.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
<rect x="3.0" y="3.0" width="0.8" height="0.8" rx="0.2" ry="0.2" fill="#E30A17" stroke="transparent" stroke-width="0.05" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB