In [1]:
import pandas as pd
from IPython.display import IFrame, HTML
import html

Fetch and preprocess json

In [2]:
user, repository = 'd3', 'd3'

data = pd.read_json(f'https://api.github.com/repos/{user}/{repository}/stats/punch_card')
data.columns = ['dow', 'hour', 'contributions']
data.head()
Out[2]:
dow hour contributions
0 0 0 7
1 0 1 0
2 0 2 1
3 0 3 0
4 0 4 0

Save to local

In [3]:
data.to_json('./data/d3-contributions.json', orient='records')

d3.js

In [4]:
!cat punchcard.html
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<style>
    div.tooltip {
        position: absolute;
        text-align: center;
        width: 60px;
        height: 28px;
        padding: 2px;
        font: 12px sans-serif;
        background: rgb(255, 255, 255);
        border: 0px;
        border-radius: 8px;
        pointer-events: none;
    }
</style>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
    const margin = {top: 20, right: 20, bottom: 50, left: 50};
    const width = 600 - margin.left - margin.right;
    const height = 200 - margin.top - margin.bottom;

    const xScale = d3.scaleLinear().range([0, width]);
    const yScale = d3.scaleLinear().rangeRound([0, height]);
    const rScale = d3.scaleSqrt().range([0, 10]);

    const dayOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat"];

    const svg = d3.select("body")
        .append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
        .append("g")
            .attr("transform", `translate(${margin.left}, ${margin.top})`);
    
    const dataPromise = d3.json("/files/contributions.json"); // inject:contributions
    dataPromise.then(data => {
        xScale.domain([-0.5, 23.5]);
        yScale.domain([-0.5, 6.5]);
        rScale.domain([0, d3.max(data, d => d.contributions)]);

        const div = d3.select("body").append("div")
            .attr("class", "tooltip")
            .style("opacity", 0);

        svg.selectAll("dot")
            .data(data)
            .enter().append("circle")
            .attr("r", d => rScale(d.contributions))
            .attr("cx", d => xScale(d.hour))
            .attr("cy", d => yScale(d.dow))
            .on('mouseover', d => {
                div.transition()
                    .duration(200)
                    .style("opacity", .9);
                div.html(`${dayOfWeek[d.dow]}, ${d.hour}<br />${d.contributions}`)
                    .style("left", (d3.event.pageX) + "px")
                    .style("top", (d3.event.pageY - 28) + "px");
            })
            .on("mouseout", d => {
                div.transition()
                .duration(500)
                .style("opacity", 0);
            });

        svg.append("g")
            .attr("transform", `translate(0, ${height})`)
            .call(d3.axisBottom(xScale).ticks(24));

        svg.append("g")
            .call(
                d3.axisLeft(yScale)
                    .ticks(7)
                    .tickFormat((d, i) => dayOfWeek[d])
            );

        svg.append("text")
            .attr("transform", `translate(${width / 2}, ${(height + margin.top + 15)})`)
            .style("text-anchor", "middle")
            .style("font-size", "12px")
            .text("hour");

        svg.append("text")
            .attr("transform", "rotate(-90)")
            .attr("y", 0 - margin.left)
            .attr("x", 0 - (height / 2))
            .attr("dy", "1em")
            .style("text-anchor", "middle")
            .style("font-size", "12px")
            .text("day of week");   
    });
</script>
</body>

単にiframeに描画

In [5]:
IFrame(src="punchcard.html", width=650, height=250)
Out[5]:

JSON を HTML に埋め込んでからiframeに描画

In [6]:
with open('punchcard.html', mode='r') as f:
    punchcard_html = ''.join(f.readlines())

json_str = data.to_json(orient='records')
punchcard_html_injected = punchcard_html.replace(
    'const dataPromise = d3.json("/files/contributions.json");',
    f'const dataPromise = Promise.resolve({json_str});')
In [7]:
IFrame(f'data:text/html;charset=utf-8,{html.escape(punchcard_html_injected)}', width=650, height=250)
Out[7]:

vega-lite

JSON を HTML に埋め込んでからiframeに描画

In [8]:
dow = ["Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat"]
json_str = (
    data
    .assign(dow = lambda d: d.dow.map(lambda i: dow[i]))
    .rename(columns={'dow': 'day of week'})
    .to_json(orient='records')
)

with open('2019-01-08_try_js_vis_tools/vega-lite/index.html', mode='r') as f:
    vegalite_html = ''.join(f.readlines())
In [9]:
print(vegalite_html)
<!DOCTYPE html>
<head>
  <title>Vega Lite Bar Chart</title>
  <meta charset="utf-8">

  <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/vega.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/vega-lite.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/vega-embed.js"></script>

</head>
<body>
  <!-- Container for the visualization -->
  <div id="vis"></div>

  <script>
  // Assign the specification to a local variable vlSpec.
  var vlSpec = {
    "$schema": "https://vega.github.io/schema/vega-lite/v3.json",
    "data": { "url": "../../data/d3-contributions.json"},
    "mark": { "type": "circle", "color": "black" },

    "encoding": {
      "y": {
        "field": "day of week",
        "type": "nominal",
        "sort": ["Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat"]
      },
      "x": {
        "field": "hour",
        "type": "nominal"
      },
      "size": {
        "field": "contributions",
        "type": "quantitative",
        "aggregate": "sum"
      }
    }
  };

  // Embed the visualization in the container with id `vis`
  vegaEmbed("#vis", vlSpec, {actions: false});
  </script>
</body>
</html>

In [10]:
vegalite_html_injected = vegalite_html.replace(
    '{ "url": "../../data/d3-contributions.json"}',
    '{ "values": ' + json_str + '}')
IFrame(f'data:text/html;charset=utf-8,{html.escape(vegalite_html_injected)}', width=700, height=220)
Out[10]:
In [ ]:
 
In [ ]: