TIBCO Spotfire使用技巧:使用JSViz制作TIBCO Spotfire的调研数据视图

在最近的不少沟通中,本问及到不少问题是关于,在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

TIBCO Spotfire使用技巧:使用JSViz制作TIBCO Spotfire的调研数据视图_第1张图片

花了几个小时,最后的图表看起来很不错。所以我们认为在JSViz中完成创建图表的过程会很好。

入门

我们需要的第一件事是一些数据,所以我从链接的站点raw_data.csv获取了样本数据,并将其转换为Spotfire stdf文件。您可以从此处下载数据文件。

加载数据后,我在我的DXP中添加了一个JSViz可视化。JSViz选择了表并创建了一个默认的数据配置,我只需要调整它以满足我的需求。由于我的图表将使用所有数据行,我将所有列添加到“数据配置”页面的两个部分,并设置列顺序和名称以匹配示例,如下所示:

接下来我需要添加一些JavaScript文件。一般我总是习惯使用JSViz附带的模板文件,并在教程中使用。所以我添加了以下文件:

  • jQuery.js
  • d3.vs.min.js
  • Template.css 更名为 PollChart.css
  • Template.js 更名为 PollChart.js
  • JSViz.js

将这些添加到JSViz中的Contents页面,保持上面的顺序,于是我们就有了如下的可视化视图:

添加图表代码

添加css只是用网站上的css代码覆盖PollChart.css的内容。

对于JS代码,与我创建的大多数可视化代码一样,我将代码分成两部分:

  • 将传入的Spotfire数据转换为图表代码所需的对象的部分。这在 renderCore() 方法中进行,并且每次数据集更改时都由Spotfire调用。
  • 获取这些对象并在屏幕上绘制它们的部分。这被拉出到一个函数drawchart()中,该函数drawchart()从 renderCore() 调用,但每当有人调整可视化对象时,也会从调整大小例程中调用它。这将取代我们当前的displayWelcomeMessage()调用。

此方法可确保在我们需要调整可视化大小时,可以使用图表数据重绘可视化。

以下是插入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)");
}

这里的主要变化是:

  • 更改代码以使用polldata对象而不是从CSV文件读取的数据
  • 将DIV目标更改为“#js_chart”
  • 使用window.innerWidth和window.innerHeight确定绘图区域的大小
  • 记住在添加新图表之前清除现有图表

有了这个,我们得到一个基本的图表:

添加调整大小逻辑

这一步非常简单,因为我们已经将图表绘制逻辑分离为它自己的函数。所以调整大小例程如下所示:

var resizing = false;

window.onresize = function (event)
{
    resizing = true;
    if ($("#js_chart"))
    {
        drawchart ();
    }
    resizing = false;
}

数据标记逻辑

添加数据标记逻辑有两个方面:

  1. 使可视化呈现标记的行和未标记的行不同。在我们的例子中,我们将未标记行的不透明度设置为0.3,因此它们看起来变暗,类似于Spotfire的行为。Spotfire提供有关是否在发送到renderCore()的数据中标记行的信息,因此我们需要将此信息存储在polldata对象中并在drawchart()例程中使用它。
     
  2. 允许用户在可视化文件上选择一行或多行,并告诉Spotfire标记这些项目。Spotfire在发送到renderCore()的数据中提供唯一的标记标识符,因此我们需要将此信息存储在polldata对象中并在markModel()例程中使用它。

添加渲染标记逻辑

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调试器来逐步查看可视化代码并找出出错的地方,或者只是在代码执行时内省变量。要启用此功能,请转到“工具” - >“选项”并滚动到“应用程序”部分的底部:

 

完成的例子

您可以从此处下载完成的示例代码和示例数据:

  • PollChart.js
  • PollChart.css
  • PollData.stdf

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(TIBCO Spotfire使用技巧:使用JSViz制作TIBCO Spotfire的调研数据视图)