Skip to content

Instantly share code, notes, and snippets.

@timelyportfolio
Last active February 20, 2021 13:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save timelyportfolio/7849312d1fe8ee75b9be0e39ef942e9b to your computer and use it in GitHub Desktop.
Save timelyportfolio/7849312d1fe8ee75b9be0e39ef942e9b to your computer and use it in GitHub Desktop.
R base graphics with d3-annotation
license: mit

Built in R, assembled with blockbuilder.org


Don't try this at home. This little bit of code (not robust and specifically hacked to work) takes a base graphics xyplot as SVG and then uses d3 and the delightful d3-annotation to label two of the points.

library(d3r)
library(htmltools)
library(pipeR)

d3_ann <- htmlDependency(
  name = "d3-annotation",
  version = "1.12.1",
  src = c(href = "https://unpkg.com/d3-svg-annotation@1.12.1/"),
  script = "d3-annotation.js",
  stylesheet = "d3-annotation.css"
)

# now let's try it with svglite
library(svglite)

plot(1:10, type="b")
rp <- recordPlot()
size = dev.size()
plt = par("plt")
usr = par("usr")

s <- svgstring(standalone=FALSE,width = size[1], height=size[2])
replayPlot(rp)
dev.off()

# this is just to make it render in RStudio
library(xml2)
svg_xml <- read_xml(as.character(s()))
cp_rect <- xml_find_first(svg_xml,"*//clipPath/rect")
cp_attr <- lapply(
  list(
    x = xml_attr(cp_rect,"x"),
    y = xml_attr(cp_rect,"y"),
    height = xml_attr(cp_rect,"height"),
    width = xml_attr(cp_rect,"width")
  ),
  as.numeric
)

# drawing with no knowledge of plot from R
#   using example1
tagList(
  tags$style(".annotation-note-bg {stroke: none;}"),
  HTML(s()),
  tags$script(HTML(
sprintf(
'
vb = d3.select("svg").attr("viewBox").split(" ");
var width = +vb[2];
var height = +vb[3];

var svg = d3.select("svg");

var margins = %s;
var usr = %s;

var type = d3.annotationLabel;

var annotations = [
{
  note: {
    label: "Point with x=3, y=3. Drag my points somewhere else.",
    title: "Annotations :)"
  },
  //can use x, y directly instead of data
  data: { x: 3, y: 3 },
  dy: -40,
  dx: 20
},
{
  note: {
    label: "x(8), y(8)",
    title: "another point"
  },
  //can use x, y directly instead of data
  data: { x: 8, y: 8 },
  dy: -40,
  dx: 20
}
]

// use clipPath rect for range of plot; multiplying usr * height
//  did not work on the y limits
//  but only because I did not research enough
//  and lazily assume that there will be a clipPath around our plot
//  and that it will be the first clipPath in our svg

// RStudio iframe security does not like following
//  so I elected to do after commented for this to work
/*
var cp = d3.select("clipPath rect");

//set domains even though skipped in original example
var x = d3.scaleLinear()
  //.range([width * margins[0], width*margins[1]])
  .range([parseFloat(cp.attr("x")), parseFloat(cp.attr("width")) + parseFloat(cp.attr("x"))])
  .domain([usr[0], usr[1]])
var y = d3.scaleLinear()
  //.range([height * margins[3], height * margins[2]])
  .range([parseFloat(cp.attr("height")) + parseFloat(cp.attr("y")), +cp.attr("y")])
  .domain([usr[0], usr[1]])
*/

var cp_attr = %s;
var x = d3.scaleLinear()
  .range([cp_attr.x, cp_attr.width + cp_attr.x])
  .domain([usr[0], usr[1]])
var y = d3.scaleLinear()
  .range([cp_attr.height + cp_attr.y, cp_attr.y])
  .domain([usr[0], usr[1]])

var makeAnnotations = d3.annotation()
  .editMode(true)
  .type(type)
  //accessors & accessorsInverse not needed
  //if using x, y in annotations JSON
  .accessors({
    x: function(d) { return x(d.x) },
    y: function(d) { return y(d.y) }
  })
  .accessorsInverse({
    x: function(d) { return x.invert(d.x) },
    y: function(d) { return y.invert(d.y) }
  })
  .annotations(annotations)

svg
  .append("g")
  .attr("class", "annotation-group")
  .call(makeAnnotations)
',
jsonlite::toJSON(plt),
jsonlite::toJSON(usr),
jsonlite::toJSON(cp_attr, auto_unbox=TRUE)
)    
  )),
  d3_dep_v4(),
  d3_ann
) %>>%
  browsable()

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<script src="https://unpkg.com/d3@4.13.0/build/d3.min.js"></script>
<link href="https://unpkg.com/d3-svg-annotation@1.12.1/d3-annotation.css" rel="stylesheet" />
<script src="https://unpkg.com/d3-svg-annotation@1.12.1/d3-annotation.js"></script>
</head>
<body style="background-color:white;">
<style>.annotation-note-bg {stroke: none;}</style>
<svg viewBox='0 0 507.75 268.50'>
<defs>
<style type='text/css'><![CDATA[
line, polyline, path, rect, circle {
fill: none;
stroke: #000000;
stroke-linecap: round;
stroke-linejoin: round;
stroke-miterlimit: 10.00;
}
]]></style>
</defs>
<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/>
<defs>
<clipPath id='cpNTkuMDR8NDc3LjUxfDE5NS4wNnw1OS4wNA=='>
<rect x='59.04' y='59.04' width='418.47' height='136.02' />
</clipPath>
</defs>
<polyline points='74.54,190.02 117.59,176.03 160.64,162.03 203.70,148.04 246.75,134.05 289.80,120.05 332.85,106.06 375.91,92.07 418.96,78.07 462.01,64.08 ' style='stroke-width: 0.75;' clip-path='url(#cpNTkuMDR8NDc3LjUxfDE5NS4wNnw1OS4wNA==)' />
<defs>
<clipPath id='cpMHw1MDcuNzV8MjY4LjV8MA=='>
<rect x='0.00' y='0.00' width='507.75' height='268.50' />
</clipPath>
</defs>
<line x1='117.59' y1='195.06' x2='462.01' y2='195.06' style='stroke-width: 0.75;' clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)' />
<line x1='117.59' y1='195.06' x2='117.59' y2='202.26' style='stroke-width: 0.75;' clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)' />
<line x1='203.70' y1='195.06' x2='203.70' y2='202.26' style='stroke-width: 0.75;' clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)' />
<line x1='289.80' y1='195.06' x2='289.80' y2='202.26' style='stroke-width: 0.75;' clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)' />
<line x1='375.91' y1='195.06' x2='375.91' y2='202.26' style='stroke-width: 0.75;' clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)' />
<line x1='462.01' y1='195.06' x2='462.01' y2='202.26' style='stroke-width: 0.75;' clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)' />
<g clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)'><text x='114.25' y='220.98' style='font-size: 12.00px; font-family: Arial;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text></g>
<g clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)'><text x='200.36' y='220.98' style='font-size: 12.00px; font-family: Arial;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text></g>
<g clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)'><text x='286.46' y='220.98' style='font-size: 12.00px; font-family: Arial;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text></g>
<g clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)'><text x='372.57' y='220.98' style='font-size: 12.00px; font-family: Arial;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text></g>
<g clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)'><text x='455.34' y='220.98' style='font-size: 12.00px; font-family: Arial;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text></g>
<line x1='59.04' y1='176.03' x2='59.04' y2='64.08' style='stroke-width: 0.75;' clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)' />
<line x1='59.04' y1='176.03' x2='51.84' y2='176.03' style='stroke-width: 0.75;' clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)' />
<line x1='59.04' y1='148.04' x2='51.84' y2='148.04' style='stroke-width: 0.75;' clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)' />
<line x1='59.04' y1='120.05' x2='51.84' y2='120.05' style='stroke-width: 0.75;' clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)' />
<line x1='59.04' y1='92.07' x2='51.84' y2='92.07' style='stroke-width: 0.75;' clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)' />
<line x1='59.04' y1='64.08' x2='51.84' y2='64.08' style='stroke-width: 0.75;' clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)' />
<g clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)'><text transform='translate(41.76,179.37) rotate(-90)' style='font-size: 12.00px; font-family: Arial;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text></g>
<g clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)'><text transform='translate(41.76,151.38) rotate(-90)' style='font-size: 12.00px; font-family: Arial;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text></g>
<g clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)'><text transform='translate(41.76,123.39) rotate(-90)' style='font-size: 12.00px; font-family: Arial;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text></g>
<g clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)'><text transform='translate(41.76,95.40) rotate(-90)' style='font-size: 12.00px; font-family: Arial;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text></g>
<g clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)'><text transform='translate(41.76,70.75) rotate(-90)' style='font-size: 12.00px; font-family: Arial;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text></g>
<polyline points='59.04,195.06 477.51,195.06 477.51,59.04 59.04,59.04 59.04,195.06 ' style='stroke-width: 0.75;' clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)' />
<g clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)'><text x='253.60' y='249.78' style='font-size: 12.00px; font-family: Arial;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>Index</text></g>
<g clip-path='url(#cpMHw1MDcuNzV8MjY4LjV8MA==)'><text transform='translate(12.96,138.73) rotate(-90)' style='font-size: 12.00px; font-family: Arial;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>1:10</text></g>
</svg>
<script>
vb = d3.select("svg").attr("viewBox").split(" ");
var width = +vb[2];
var height = +vb[3];
var svg = d3.select("svg");
var margins = [0.1163,0.9404,0.2735,0.7801];
var usr = [0.64,10.36,0.64,10.36];
var type = d3.annotationLabel;
var annotations = [
{
note: {
label: "Point with x=3, y=3. Drag my points somewhere else.",
title: "Annotations :)"
},
//can use x, y directly instead of data
data: { x: 3, y: 3 },
dy: -40,
dx: 20
},
{
note: {
label: "x(8), y(8)",
title: "another point"
},
//can use x, y directly instead of data
data: { x: 8, y: 8 },
dy: -40,
dx: 20
}
]
// use clipPath rect for range of plot; multiplying usr * height
// did not work on the y limits
// but only because I did not research enough
// and lazily assume that there will be a clipPath around our plot
// and that it will be the first clipPath in our svg
// RStudio iframe security does not like following
// so I elected to do after commented for this to work
/*
var cp = d3.select("clipPath rect");
//set domains even though skipped in original example
var x = d3.scaleLinear()
//.range([width * margins[0], width*margins[1]])
.range([parseFloat(cp.attr("x")), parseFloat(cp.attr("width")) + parseFloat(cp.attr("x"))])
.domain([usr[0], usr[1]])
var y = d3.scaleLinear()
//.range([height * margins[3], height * margins[2]])
.range([parseFloat(cp.attr("height")) + parseFloat(cp.attr("y")), +cp.attr("y")])
.domain([usr[0], usr[1]])
*/
var cp_attr = {"x":59.04,"y":59.04,"height":136.02,"width":418.47};
var x = d3.scaleLinear()
.range([cp_attr.x, cp_attr.width + cp_attr.x])
.domain([usr[0], usr[1]])
var y = d3.scaleLinear()
.range([cp_attr.height + cp_attr.y, cp_attr.y])
.domain([usr[0], usr[1]])
var makeAnnotations = d3.annotation()
.editMode(true)
.type(type)
//accessors & accessorsInverse not needed
//if using x, y in annotations JSON
.accessors({
x: function(d) { return x(d.x) },
y: function(d) { return y(d.y) }
})
.accessorsInverse({
x: function(d) { return x.invert(d.x) },
y: function(d) { return y.invert(d.y) }
})
.annotations(annotations)
svg
.append("g")
.attr("class", "annotation-group")
.call(makeAnnotations)
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment