在最近的不少沟通中,本问及到不少问题是关于,在TIBCO Spotfire如何去实现一些非Spotfire自带的视图,比如桑基图(sankey), Survey Data图等,如下使用技巧文章就是以制作 Survey Data为例告诉各位童鞋如何通过JSViz的方式来制作Spotfire非开箱即用的一些第三方图。
译文原文在:https://community.tibco.com/wiki/tibco-spotfire-survey-data-chart-using-jsviz
目录
最近有一位客户要求TIBCO Support使用JSViz在Spotfire中创建以下图表:
http://bl.ocks.org/wpoely86/e285b8e4c7b84710e463
花了几个小时,最后的图表看起来很不错。所以我们认为在JSViz中完成创建图表的过程会很好。
我们需要的第一件事是一些数据,所以我从链接的站点raw_data.csv获取了样本数据,并将其转换为Spotfire stdf文件。您可以从此处下载数据文件。
加载数据后,我在我的DXP中添加了一个JSViz可视化。JSViz选择了表并创建了一个默认的数据配置,我只需要调整它以满足我的需求。由于我的图表将使用所有数据行,我将所有列添加到“数据配置”页面的两个部分,并设置列顺序和名称以匹配示例,如下所示:
接下来我需要添加一些JavaScript文件。一般我总是习惯使用JSViz附带的模板文件,并在教程中使用。所以我添加了以下文件:
将这些添加到JSViz中的Contents页面,保持上面的顺序,于是我们就有了如下的可视化视图:
添加css只是用网站上的css代码覆盖PollChart.css的内容。
对于JS代码,与我创建的大多数可视化代码一样,我将代码分成两部分:
此方法可确保在我们需要调整可视化大小时,可以使用图表数据重绘可视化。
以下是插入renderCore()的代码:
var polldata = [];
var color = d3.scale.ordinal()
.range(["#c7001e", "#f6a580", "#cccccc", "#92c6db", "#086fad"])
.domain(["Strongly disagree", "Disagree", "Neither agree nor disagree", "Agree", "Strongly agree"]);
var svg;
//
// Main Drawing Method
//
function renderCore(sfdata)
{
if (resizing) {
return;
}
// Log entering renderCore
log ( "Entering renderCore" );
// Extract the columns
var columns = sfdata.columns;
// Extract the data array section
var chartdata = sfdata.data;
// count the marked rows in the data set, needed later for marking rendering logic
var markedRows = 0;
for (var i = 0; i < chartdata.length; i++) {
if (chartdata[i].hints.marked) {
markedRows = markedRows + 1;
}
}
polldata = [];
for ( var nIndex = 0 ; nIndex < chartdata.length ; nIndex++ )
{
var items = chartdata[nIndex].items;
var pollrow = items;
pollrow.Question = items[0];
pollrow["Strongly disagree"] = +items[1]*100/+items[6];
pollrow["Disagree"] = +items[2]*100/+items[6];
pollrow["Neither agree nor disagree"] = +items[3]*100/+items[6];
pollrow["Agree"] = +items[4]*100/+items[6];
pollrow["Strongly agree"] = +items[5]*100/items[6];
var x0 = -1*(pollrow["Neither agree nor disagree"]/2+pollrow["Disagree"]+pollrow["Strongly disagree"]);
var idx = 0;
pollrow.boxes = color.domain().map(function(name) { return {name: name, x0: x0, x1: x0 += +pollrow[name], N: +pollrow[6], n: +pollrow[idx += 1] }; });
polldata.push ( pollrow );
}
drawchart ();
wait ( sfdata.wait, sfdata.static );
}
而drawchart()代码如下所示:
function drawchart ()
{
var width = window.innerWidth * 0.95;
var height = window.innerHeight * 0.95;
var margin = {top: 50, right: 20, bottom: 10, left: 85},
width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;
var y = d3.scale.ordinal()
.rangeRoundBands([0, height], .3);
var x = d3.scale.linear()
.rangeRound([0, width]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("top");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
d3.select("#d3-plot").remove ();
svg = d3.select("#js_chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("id", "d3-plot")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var min_val = d3.min(polldata, function(d) {
return d.boxes["0"].x0;
});
var max_val = d3.max(polldata, function(d) {
return d.boxes["4"].x1;
});
x.domain([min_val, max_val]).nice();
y.domain(polldata.map(function(d) { return d.Question; }));
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
var vakken = svg.selectAll(".question")
.data(polldata)
.enter().append("g")
.attr("class", "bar")
.attr("transform", function(d) { return "translate(0," + y(d.Question) + ")"; });
var bars = vakken.selectAll("rect")
.data(function(d) { return d.boxes; })
.enter().append("g").attr("class", "subbar");
bars.append("rect")
.attr("height", y.rangeBand())
.attr("x", function(d) { return x(d.x0); })
.attr("width", function(d) { return x(d.x1) - x(d.x0); })
.style("fill", function(d) { return color(d.name); });
bars.append("text")
.attr("x", function(d) { return x(d.x0); })
.attr("y", y.rangeBand()/2)
.attr("dy", "0.5em")
.attr("dx", "0.5em")
.style("font" ,"10px sans-serif")
.style("text-anchor", "begin")
.text(function(d) { return d.n !== 0 && (d.x1-d.x0)>3 ? d.n : "" });
vakken.insert("rect",":first-child")
.attr("height", y.rangeBand())
.attr("x", "1")
.attr("width", width)
.attr("fill-opacity", "0.5")
.style("fill", "#F5F5F5")
.attr("class", function(d,index) { return index%2==0 ? "even" : "uneven"; });
svg.append("g")
.attr("class", "y axis")
.append("line")
.attr("x1", x(0))
.attr("x2", x(0))
.attr("y2", height);
var startp = svg.append("g").attr("class", "legendbox").attr("id", "mylegendbox");
// this is not nice, we should calculate the bounding box and use that
var legend_tabs = [0, 120, 200, 375, 450];
var legend = startp.selectAll(".legend")
.data(color.domain().slice())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(" + legend_tabs[i] + ",-45)"; });
legend.append("rect")
.attr("x", 0)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", 22)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "begin")
.style("font" ,"10px sans-serif")
.text(function(d) { return d; });
d3.selectAll(".axis path")
.style("fill", "none")
.style("stroke", "#000")
.style("shape-rendering", "crispEdges")
d3.selectAll(".axis line")
.style("fill", "none")
.style("stroke", "#000")
.style("shape-rendering", "crispEdges")
var movesize = width/2 - startp.node().getBBox().width/2;
d3.selectAll(".legendbox").attr("transform", "translate(" + movesize + ",0)");
}
这里的主要变化是:
有了这个,我们得到一个基本的图表:
这一步非常简单,因为我们已经将图表绘制逻辑分离为它自己的函数。所以调整大小例程如下所示:
var resizing = false;
window.onresize = function (event)
{
resizing = true;
if ($("#js_chart"))
{
drawchart ();
}
resizing = false;
}
添加数据标记逻辑有两个方面:
Spotfire传递一个标志,指示是否在传递给renderCore()的数据的“提示”部分中标记了一个项目。如果我们在表可视化中标记第一行数据,并查看传递给JSViz的前两行JSON数据,我们可以看到第一行设置了“标记”标志,并且两行都有标记ID:
...
"data": [
{
"items": [
"Question 1",
24,
294,
594,
1927,
376,
3215
],
"hints": {
"marked": true,
"index": 0
}
},
{
"items": [
"Question 2",
2,
2,
0,
7,
0,
11
],
"hints": {
"index": 1
}
},
...
因此,我们将代码添加到renderCore()方法中,以将这些值存储为每个polldata对象的属性,如下所示
for ( var nIndex = 0 ; nIndex < chartdata.length ; nIndex++ )
{
var items = chartdata[nIndex].items;
//
// Marking Index and Marked Flag
//
var markid = chartdata[nIndex].hints.index;
var marked = chartdata[nIndex].hints.marked ? true : false;
...
pollrow.boxes = color.domain().map(function(name) { return {name: name, x0: x0, x1: x0 += +pollrow[name], N: +pollrow[6], n: +pollrow[idx += 1], markid: markid, marked: marked}; });
polldata.push ( pollrow );
}
现在我们有了这些数据,我们可以在drawchart()函数中使用它来改变每行的不透明度,如下所示
...
bars.append("rect")
.attr("height", y.rangeBand())
.attr("x", function(d) { return x(d.x0); })
.attr("width", function(d) { return x(d.x1) - x(d.x0); })
.style("fill", function(d) { return color(d.name); })
.attr("opacity", function (d, i) //Spotfire style faded marking coloring
{
if ( markedRows != 0 && !d.marked )
{
return (0.3);
}
else
{
return (1);
}
});
...
这里的逻辑是,如果没有标记任何行,则所有项目都以完全可见性显示。为此,我们需要使用在renderCore()中创建的markedRows变量。不幸的是,无法从drawchart()函数访问它,因此我们需要返回并将markedRows移动到全局范围。确保从renderCore()中的markedRows变量赋值前删除“var”关键字!
var polldata = [];
var color = d3.scale.ordinal()
.range(["#c7001e", "#f6a580", "#cccccc", "#92c6db", "#086fad"])
.domain(["Strongly disagree", "Disagree", "Neither agree nor disagree", "Agree", "Strongly agree"]);
var markedRows = 0;
var svg;
...
function renderCore(sfdata)
{
...
// count the marked rows in the data set, needed later for marking rendering logic
markedRows = 0;
...
这给了我们熟悉的标记行为:
JSViz默认提供标准的矩形选择机制。为了使用它,我们只需要为markModel()函数提供一个实现。在我们的例子中,逻辑是找到页面上的哪些对象与标记矩形相交,并通过调用markIndices()将它们的标记ID提交给Spotfire。这是我们的markModel()函数的代码:
function markModel(markMode, rectangle)
{
var selsvg = d3.select ( "svg" );
if ( !selsvg )
{
return;
}
var indicesToMark = [];
var markData = {};
markData.markMode = markMode;
svgElem = selsvg[0][0];
var markRect = svgElem.createSVGRect();
markRect.x = rectangle.x;
markRect.y = rectangle.y;
markRect.height = rectangle.height; // + one to get the item under the click
markRect.width = rectangle.width; // + one to get the item under the click
var elements = svgElem.getIntersectionList ( markRect, svgElem );
for (var index = 0; index < elements.length; index = index + 1)
{
element = elements[index];
if ( element.__data__ && element.__data__.boxes )
{
if ( element.__data__.boxes.length > 0 )
{
indicesToMark.push ( element.__data__.boxes[0].markid );
}
}
}
markData.indexSet = indicesToMark;
markIndices ( markData );
}
使用此代码,我们可以在图表上标记项目,并更新Spotfire中的标记集。
在JSViz中开发代码时,我建议打开“开发”菜单,它允许您使用内置的Chromium调试器来逐步查看可视化代码并找出出错的地方,或者只是在代码执行时内省变量。要启用此功能,请转到“工具” - >“选项”并滚动到“应用程序”部分的底部:
您可以从此处下载完成的示例代码和示例数据: