所以,我希望标题能让您了解我想要实现的目标。我每个月都会做一份报告,详细说明我们环境中的系统中断情况。数据非常基本 - 系统名称、开始时间、结束时间、中断持续时间...
我的做法是将其带到 Illustrator(我使用 R、RMarkdown 和我所有的其他报告,这是我唯一无法弄清楚如何自动化的部分)并创建一个“时间线图”,同时显示停机的持续时间。每个系统都有不同的颜色(由系统名称决定) - 时间线按年份表示 - 小刻度表示月份。持续时间(圆圈)让人联想到“棒棒糖”时间线...我附上了一个基本图像来展示这一点:
我希望有某种方法可以在 Latex 中生成这个,并输入来自 CSV 文件的数据......我唯一的要求(主要是为了可读性)是:
- 气泡不重叠 - 这就是它们在同一张图片上高度不同的原因。高度除了允许气泡不重叠之外没有其他意义。
- 标签只需要系统名称和停机时长。我不在乎标签是在气泡内还是气泡外,只要它们不重叠且易于阅读即可。
如果有一种方法可以让 Latex 确定配色方案,那就很好,我不会被这些颜色所束缚,或者,如果可能的话,CVS 中的某个字段可以是颜色(HEX 或我需要的任何颜色代码类型)。
我将非常感激任何关于如何在 Latex 中创建它的提示或示例。我很想实现自动化,因为我的其他报告都是在 RMarkdown、Latex 或某种组合中完成的,所以我想我会从这里开始。感谢任何人提供的帮助!!
编辑:
有人请求提供样本数据集 - 这里是包含我正在处理的数据的 CSV 链接。抱歉耽误了这么久,我不得不运行新查询并清除敏感信息。这里是:
答案1
以下是使用以下方法实现此可视化的尝试渐近线。该算法非常简单:
对于每个数据点,都会构造其标签,最好具有相同的宽度和长度(以便更频繁地适合气泡内)
所有数据点按顺序处理,并选择当前杆高度作为气泡和标签与先前处理的气泡和标签不重叠的最小值
以下是代码:
import graph;
Label FitLabel(string text, real width0, real width1, real height0) {
if (width1 - width0 < 1pt)
return Label(minipage(text, width1), align=E, filltype=UnFill(0.5pt));
real width2 = (width0 + width1) / 2;
frame f;
label(f, minipage(text, width2));
real height = max(f).y - min(f).y;
if (height <= height0)
return Label(minipage(text, width1), align=E, filltype=UnFill(0.5pt));
if (width2 <= height)
return FitLabel(text, width2, width1, height0);
else
return FitLabel(text, width0, width2, height);
}
Label FitLabel(string text) {
frame f;
label(f, text);
real width = max(f).x - min(f).x;
real height = max(f).y - min(f).y;
if (width <= height)
return Label(text, align=E, filltype=UnFill(0.5pt));
return FitLabel(text, 0, width, height);
}
struct Lollipop {
int time; // time in seconds since epoch
real height; // handle height
real radius; // bubble radius
Label label; // bubble label
real width; // label width
pen color;
path bubble;
pair min;
pair max;
bool inside; // is the label inside
static Lollipop Lollipop(string date, real minstem, real area, string label, pen color) {
Lollipop l = new Lollipop;
l.time = seconds(date, "%Y-%m-%d");
l.height = minstem;
l.radius = sqrt(area/pi);
l.label = FitLabel(label);
l.color = color;
l.bubble = scale(l.radius)*shift(0,1)*unitcircle;
l.min = min(l.bubble);
l.max = max(l.bubble);
frame f;
label(f, l.label);
pair fmin = min(f);
pair fmax = max(f);
real dist = sqrt((fmax.x-fmin.x)^2 + (fmax.y-fmin.y)^2);
if (dist >= 2*l.radius - 1pt) {
l.inside = false;
l.max += (fmax.x, 0);
if (fmax.y-fmin.y > l.max.y-l.min.y) {
l.min = (l.min.x, (l.min.y+l.max.y+fmin.y-fmax.y)/2);
l.max = (l.max.x, (l.min.y+l.max.y-fmin.y+fmax.y)/2);
l.height = -l.min.y+minstem;
}
} else {
l.label = Label(l.label, align=Center);
l.inside = true;
}
return l;
}
void DrawStem(real dx) {
draw(shift(this.time*dx,0)*((0,0)--(0,this.height)), this.color+linewidth(1));
}
void DrawBubble(real dx) {
path p = shift(this.time*dx,this.height)*this.bubble;
fill(p, this.color);
if (this.inside)
label(this.label, (this.time*dx,this.height+this.radius));
else
label(this.label, max(p) - (0, (max(p).y-min(p).y)/2));
}
}
from Lollipop unravel Lollipop;
Lollipop[] FromCSV(string filename, real scale=1) {
int nfields = 6;
Lollipop[] res;
file fd = input(filename);
string[] data = fd.csv();
int i = 0;
for(int row = 0; row < data.length/nfields; ++row) {
real Area = (real) data[i+1];
real Red = (real) data[i+2];
real Blue = (real) data[i+3];
real Green = (real) data[i+4];
Lollipop l = Lollipop(data[i], max(scale,10), Area*scale^2, data[i+5], rgb(Red, Blue, Green));
res.push(l);
i = i + nfields;
}
return res;
}
bool less(Lollipop a, Lollipop b) {
return a.height+a.min.y < b.height+b.min.y;
}
bool overlap(Lollipop a, Lollipop b, real dx, real delta) {
if (a.time*dx+a.min.x > b.time*dx+b.max.x + 2*delta || 2*delta + a.time*dx+a.max.x < b.time*dx+b.min.x) {
return false;
}
if (a.height+a.min.y > b.height+b.max.y + 2*delta || 2*delta + a.height+a.max.y < b.height+b.min.y) {
return false;
}
return true;
}
real[] CreateTicks(int mintime, int maxtime, real dx) {
real[] Ticks;
int minyear = (int) time(mintime, "%Y");
for(int year = minyear; true; ++year) {
for(int month = 1; month <= 12; ++month) {
int secs = seconds(format("%d-",year)+format("%d-01", month), "%Y-%m-%d");
if(secs > maxtime+5*31*24*60*60) {
return Ticks;
}
if(secs >= mintime) {
Ticks.push(secs*dx);
}
}
}
return Ticks;
}
void DrawLollipopDiagram(string filename, real scale, real width, real delta=3pt) {
Lollipop[] data = FromCSV(filename, scale);
int mintime = data[0].time;
int maxtime = mintime;
for(Lollipop l : data) {
if (mintime > l.time) {
mintime = l.time;
}
if (maxtime < l.time) {
maxtime = l.time;
}
}
real dx = width / (maxtime - mintime);
Lollipop[] processed;
for(Lollipop l : data) {
for(Lollipop m : processed) {
if (overlap(l, m, dx, delta)) {
l.height = m.height + m.max.y - l.min.y + 2*delta;
}
}
processed.push(l);
processed = sort(processed, less);
}
for(Lollipop l : data[reverse(data.length)]) {
l.DrawStem(dx);
}
for(Lollipop l : data) {
l.DrawBubble(dx);
}
real[] Ticks = CreateTicks(mintime, maxtime, dx);
xaxis(ticks=RightTicks(format=Label(align=NE),
ticklabel=new string(real x) {return time((int)(x/dx)," %b");},
Ticks=Ticks));
}
DrawLollipopDiagram("convoutages.csv", 0.3, 1800);
是convoutages.csv
转换后的Outages.csv
。以下是前几行:
2017-01-03,300,0.14914345375687976,0.6540272918781392,0.23669459588671782,System~1
2017-01-04,900,0.12607306806653415,0.9100549942394974,0.2942881832338349,System~2
2017-01-04,900,0.10149561106296984,0.8367351353339549,0.0074195577797571,System~3
2017-01-04,900,0.7005076043775806,0.43130677399752043,0.9729505763263211,System~4
2017-01-04,1560,0.3803363164795266,0.31247107140369296,0.7012970818678369,System~1
2017-01-05,5160,0.7000549527351069,0.8235906189417422,0.08753255386256266,System~2
2017-01-05,5160,0.15963276809064333,0.9479332994427221,0.914963733830938,System~3
字段:,,,,,date
(目前我只是随机生成颜色成分),(适用通常的 LaTeX 约定,例如不间断空格)。outage duration
red
green
blue
label
~
为了得到结果你应该运行
asy -f pdf lollipop.asy
结果: