Lawrence Technological University
College of Arts and Science
Department of Mathematics and Computer Sciences


A Slopegraph -- Self Referral Loopholes and Treatment Selection

   "Urologists' Use of Intensity-Modulated Radiation Therapy for Prostate Cancer," Jean M. Mitchell, Ph.D., The New England Journal of Medicine, October 24, 2013;369:1629-37, showed that reimbursement loopholes have a significant effect on treatment selection. The stacked bar charts in this article clarify the secondary point that almost all of the growth in use of IMRT was in self referrals. Would a slopegraph like those introduced by Edward Tufte help illustrate the magnitude of the primary point?

   Laying out the slopegraph requires a little trial and error to keep the output to less than a page but big enough to accurately represent the magnitude of the slopes and preserve the relationships among each slope and its end labels.

   R would be a good place to start. R already has some libraries that help with slopegraphs. This is another approach -- mixing two widely available pieces of software, Ruby and LaTeX (Ruby 1.9.3 and TeX Live 2013.) The TeX Live Utility allows you to add the Gill Sans like font to the installed packages. The gillium package was only just added to LaTeX in December, 2013.

   Gurari's DraTeX facilitates accurately aligning text by drawing a bounding box around the text and providing an ancillary coordinate system with [0,0] at the horizontal center of the text on its baseline.

\EntryExit(1,0,1,0) \Text(--Percentage of Cases--)
% This LaTeX reads: Draw the text "Percentage of Cases."  Assume the pen
% is at ancillary coordinates [1,0] (i.e. the center of the right edge of
% the text bounding box) when the drawing starts and returns there when the
% drawing ends.

   A Ruby program:

# slopegraph.rb
   A Ruby program to lay out a slopegraph.  The Ruby program's output
is TeX/LaTeX with Eitan Gurari's DraTeX package and Hirwen Harendal's
Gillium package.  The scaling and grouping calculations are done in
Ruby, leaving just the drawing of the slopes and the typesetting of
the groups of labels to be completed in LaTeX.

   For this layout program there are 4 somewhat arbitrary design decisions:
1. Lines of labels are no closer than \baselineskip (12pts for 10pt font).
2. The scale will be increased until no more than 3 labels are placed
   in a single group because they are separated by less than \baselineskip
3. For the first guess at scale we assume the y values are a little less
   evenly divided than the labels in a bump chart.
4. Ruby's oddly named "inject" method may be only appreciated by old
   C programmers who miss counted "for" loops.  e.g.
   @labels.inject(0.0) {|sum,l| sum + l[0]} / @labels.size 
   This program will also use this construct to build columns of labels
   from the list of slopes.
# Constants used in creating the TeX source file.
FileName = "SRvNSR"
TexBegin = <<HEADER
% slopegraph.tex

  A slopegraph to illustrate a ``difference-in-differences analysis to
  evaluate changes in IMRT use according to self-referral status.''
  Data: ``Urologists' Use of Intensity-Modulated Radiation Therapy for
  Prostate Cancer,'' Jean M. Mitchell, Ph.D., \\textit{The New
  England Journal of Medicine}, October 24, 2013;369:1629-37.
  Treatment selection in urology practices that own IMRT
  facilities and self refer \\textit{v.} matched practices which do
  not own IMRT facilities or self refer.\\\\
  Chart source code:

TexEnd = <<FOOTER
TimeLabel = ["\\EntryExit(1,0,1,0) \\Text(--\\hfill Percentage of Cases in~~" +
             "\\hfill Pre-Ownership Period--)",
             "\\EntryExit(-1,0,-1,0) \\Text(--Percentage of cases in \\hfill" +
             "~~Ownership Period \\hfill--)"]
# Slope data: label => [value-time-0,value-time-1]
Slopes = {"SR IMRT" => [13.1,38.6],
          "SR Brachytherapy" => [18.6,5.6],
          "SR Prostatectomy" => [17.7,16.6],
          "SR Androgen deprivation" => [16.5,8.4],
          "SR Active Surveillance" => [26.7,27.0],
          "SR Other" => [7.3,3.9],
          "no SR IMRT" => [14.3,15.6],
          "no SR Brachytherapy" => [18.9,17.9],
          "no SR Prostatectomy" => [21.9,23.8],
          "no SR Androgen deprivation" => [15.6,11.4],
          "no SR Active Surveillance" => [26.1,27.4],
          "no SR Other" => [3.2,3.9]}
# Constants for the layout of the chart.
SlopeWidth = 100 # pts
Time0x = 50 # pts
MaxGroup = 3
Baselineskip = 12
ScaleMultiplier = 1.1
HighValue = Slopes.values.flatten.max
LowValue = Slopes.values.flatten.min
RangeValues = HighValue - LowValue
# Non-constant global with units of pts / percent.
$scale = Slopes.size * Baselineskip * ScaleMultiplier / RangeValues
# A container class for groups of labels that are close together and are
# drawn as a single node centered at the average value of the group.
class LabelNode
  def initialize(label)
    @labels =  [label]

  def <<(label)
    @labels << label

  def avg
    @labels.inject(0.0) {|sum,l| sum + l[0]} / @labels.size 

  def near?(label)
    (label[0] - avg).abs < Baselineskip * (@labels.size + 1) / 2.0 / $scale

  def draw(time)
    if time == 0
      "\\EntryExit(1,0,1,0) " +
      "\\Text(--#{{|l| "\\hfill " + l[1]}.join("~~")}--)"
    else # time == 1
      "\\EntryExit(-1,0,-1,0) " +
      "\\Text(--#{{|l|  l[1] + " \\hfill"}.join("~~")}--)"
# Prepare the 2 lists of slope labels from the list of slopes.
labels = []
# For x = time 0, in ascending order: [value, "label value"]
labels[0] = Slopes.sort{|a,b| a[1][0] <=> b[1][0]}.inject([]) do |lbs,s|
  lbs << [s[1][0],s[0] + " " + s[1][0].to_s]
# For x = time 1, in ascending order: [value, "value label"]
labels[1] = Slopes.sort{|a,b| a[1][1] <=> b[1][1]}.inject([]) do |lbs,s|
  lbs << [s[1][1],s[1][1].to_s + " " + s[0]]
# Group labels in each list less than baselineskip * scale apart.
crowded = true
while crowded
  max_len = 1
  label_nodes = []
  (0..1).each do |i|
    (0...labels[i].size).each do |j|
      if !label_nodes[i]
        label_nodes[i] = [[i][j])]
      elsif label_nodes[i][-1].near?(labels[i][j])
        len = (label_nodes[i][-1] << labels[i][j]).size
        max_len = len if len > max_len
        label_nodes[i] += [[i][j])]
  # Summarize each pass.
  puts "Scale is #{$scale} pts/percent."
  puts "Maximum labels in a vertical grouping is #{max_len}"
  if max_len > MaxGroup
    crowded = true
    $scale *= ScaleMultiplier
    crowded = false
  # p label_nodes # Uncomment for debugging.

# Prepare the TeX file.
open(FileName + ".tex","w") do |f|
  f.puts TexBegin
  Slopes.each do |s|
    f.puts "\\MoveTo(#{Time0x},#{(s[1][0] - LowValue) * $scale})"
    f.puts "\\LineTo(#{Time0x + SlopeWidth},#{(s[1][1] - LowValue) * $scale})"
  (0..1).each do |i|
    label_nodes[i].each do |n|
      f.puts "\\MoveTo(#{Time0x - 5 + (SlopeWidth + 10) * i}," +
             "#{(n.avg - LowValue) * $scale})"
      f.puts n.draw(i)
    # Print column header.
    f.puts "\\MoveTo(#{Time0x - 5 + (SlopeWidth + 10) * i}," +
           "#{RangeValues * $scale + Baselineskip * 2})"
    f.puts TimeLabel[i]
  f.puts TexEnd
# Prepare the output using LaTeX.
puts `pdflatex -output-format pdf #{FileName}.tex`
puts `pdflatex -output-format dvi #{FileName}.tex`
puts `dvipng -T tight -D 150 -o #{FileName}.png #{FileName}.dvi`

   The output

Revised January 5, 2014