Skip to content

Instantly share code, notes, and snippets.

@janhebnes
Created December 16, 2015 15:19
Show Gist options
  • Save janhebnes/67d882fa0c066b28f3cd to your computer and use it in GitHub Desktop.
Save janhebnes/67d882fa0c066b28f3cd to your computer and use it in GitHub Desktop.
D3 visualization of Steam Group relations between friends and games using Steam API - see the results at http://netbyte.dk/steam/games/ and http://netbyte.dk/steam/
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<link type="text/css" rel="stylesheet" href="style.css"/>
<style type="text/css">
path.arc {
cursor: move;
fill: #fff;
}
.node {
font-size: 10px;
}
.node:hover {
fill: #1f77b4;
}
.link {
fill: none;
stroke: #1f77b4;
stroke-opacity: .4;
pointer-events: none;
}
.link.source, .link.target {
stroke-opacity: 1;
stroke-width: 2px;
}
.node.target {
fill: #d62728 !important;
}
.link.source {
stroke: #d62728;
}
.node.source {
fill: #2ca02c;
}
.link.target {
stroke: #2ca02c;
}
</style>
</head>
<body>
<h2>
Flare imports<br>
hierarchical edge bundling
</h2>
<div style="position:absolute;bottom:0;font-size:18px;">tension: <input style="position:relative;top:3px;" type="range" min="0" max="100" value="85"></div>
<script type="text/javascript" src="d3/d3.js"></script>
<script type="text/javascript" src="d3/d3.layout.js"></script>
<script type="text/javascript" src="packages.js"></script>
<script type="text/javascript">
var w = 1920,
h = 1600,
rx = w / 2,
ry = h / 2,
m0,
rotate = 0;
var splines = [];
var cluster = d3.layout.cluster()
.size([360, ry - 120])
.sort(function(a, b) { return d3.ascending(a.key, b.key); });
var bundle = d3.layout.bundle();
var line = d3.svg.line.radial()
.interpolate("bundle")
.tension(.85)
.radius(function(d) { return d.y; })
.angle(function(d) { return d.x / 180 * Math.PI; });
// Chrome 15 bug: <http://code.google.com/p/chromium/issues/detail?id=98951>
var div = d3.select("body").insert("div", "h2")
.style("top", "-80px")
.style("left", "-160px")
.style("width", w + "px")
.style("height", w + "px")
.style("position", "absolute")
.style("-webkit-backface-visibility", "hidden");
var svg = div.append("svg:svg")
.attr("width", w)
.attr("height", w)
.append("svg:g")
.attr("transform", "translate(" + rx + "," + ry + ")");
svg.append("svg:path")
.attr("class", "arc")
.attr("d", d3.svg.arc().outerRadius(ry - 120).innerRadius(0).startAngle(0).endAngle(2 * Math.PI))
.on("mousedown", mousedown);
d3.json("flare-steamgamerelations.txt", function (classes) {
var nodes = cluster.nodes(packages.root(classes)),
links = packages.imports(nodes),
splines = bundle(links);
var path = svg.selectAll("path.link")
.data(links)
.enter().append("svg:path")
.attr("class", function (d) {
if ((typeof d != 'undefined') && (typeof d.source != 'undefined') && (typeof d.target != 'undefined')) {
return "link source-" + d.source.key + " target-" + d.target.key;
};
return "";
})
.attr("d", function(d, i) { return line(splines[i]); });
svg.selectAll("g.node")
.data(nodes.filter(function(n) { return !n.children; }))
.enter().append("svg:g")
.attr("class", "node")
.attr("id", function(d) { return "node-" + d.key; })
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
.append("svg:text")
.attr("dx", function(d) { return d.x < 180 ? 8 : -8; })
.attr("dy", ".31em")
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; })
.text(function(d) { return d.alias; })
.on("mouseover", mouseover)
.on("mouseout", mouseout);
d3.select("input[type=range]").on("change", function() {
line.tension(this.value / 100);
path.attr("d", function(d, i) { return line(splines[i]); });
});
});
d3.select(window)
.on("mousemove", mousemove)
.on("mouseup", mouseup);
function mouse(e) {
return [e.pageX - rx, e.pageY - ry];
}
function mousedown() {
m0 = mouse(d3.event);
d3.event.preventDefault();
}
function mousemove() {
if (m0) {
var m1 = mouse(d3.event),
dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
div.style("-webkit-transform", "translateY(" + (ry - rx) + "px)rotateZ(" + dm + "deg)translateY(" + (rx - ry) + "px)");
}
}
function mouseup() {
if (m0) {
var m1 = mouse(d3.event),
dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
rotate += dm;
if (rotate > 360) rotate -= 360;
else if (rotate < 0) rotate += 360;
m0 = null;
div.style("-webkit-transform", null);
svg
.attr("transform", "translate(" + rx + "," + ry + ")rotate(" + rotate + ")")
.selectAll("g.node text")
.attr("dx", function(d) { return (d.x + rotate) % 360 < 180 ? 8 : -8; })
.attr("text-anchor", function(d) { return (d.x + rotate) % 360 < 180 ? "start" : "end"; })
.attr("transform", function(d) { return (d.x + rotate) % 360 < 180 ? null : "rotate(180)"; });
}
}
function mouseover(d) {
svg.selectAll("path.link.target-" + d.key)
.classed("target", true)
.each(updateNodes("source", true));
svg.selectAll("path.link.source-" + d.key)
.classed("source", true)
.each(updateNodes("target", true));
}
function mouseout(d) {
svg.selectAll("path.link.source-" + d.key)
.classed("source", false)
.each(updateNodes("target", false));
svg.selectAll("path.link.target-" + d.key)
.classed("target", false)
.each(updateNodes("source", false));
}
function updateNodes(name, value) {
return function(d) {
if (value) this.parentNode.appendChild(this);
svg.select("#node-" + d[name].key).classed(name, value);
};
}
function cross(a, b) {
return a[0] * b[1] - a[1] * b[0];
}
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1];
}
</script>
</body>
</html>
(function() {
packages = {
// Lazily construct the package hierarchy from class names.
root: function(classes) {
var map = {};
function find(name, data) {
var node = map[name], i;
if (!node) {
node = map[name] = data || {name: name, children: []};
if (name.length) {
node.parent = find(name.substring(0, i = name.lastIndexOf(".")));
node.parent.children.push(node);
node.key = name.substring(i + 1);
}
}
return node;
}
classes.forEach(function(d) {
find(d.name, d);
});
return map[""];
},
// Return a list of imports for the given array of nodes.
imports: function(nodes) {
var map = {},
imports = [];
// Compute a map from name to node.
nodes.forEach(function(d) {
map[d.name] = d;
});
// For each import, construct a link from the source to target node.
nodes.forEach(function(d) {
if (d.imports) d.imports.forEach(function(i) {
imports.push({source: map[d.name], target: map[i]});
});
});
return imports;
}
};
})();
using System.IO;
namespace ConsoleApplication4_steamapi
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Policy;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.Xml.Serialization;
class Program
{
#region Fields
private static string groupname = "netbyte";
private static string key = "xxx";
#endregion Fields
#region Methods
private static IEnumerable<Friend> GetFriendList(string steamplayerid)
{
string url = string.Format("http://api.steampowered.com/ISteamUser/GetFriendList/v0001/?key={0}&steamid={1}&relationship=friend&format=xml", key, steamplayerid);
var memberslistxml = XDocument.Load(url);
var element = memberslistxml.Element("friendslist");
if (element == null) yield break;
var xElement = element.Element("friends");
if (xElement == null) yield break;
foreach (var node in xElement.Descendants("friend"))
{
var friend = new Friend();
var steamid = node.Element("steamid");
if (steamid != null) friend.SteamId = steamid.Value;
var relationship = node.Element("relationship");
if (relationship != null) friend.Relationship = relationship.Value;
var friendsince = node.Element("friend_since");
if (friendsince != null) friend.FriendSince = Convert.ToInt32(friendsince.Value);
if (friend.SteamId != null && friend.Relationship != null && friend.FriendSince.HasValue)
{
yield return friend;
}
}
}
private static IEnumerable<string> GetGroupMembers(string groupname)
{
string url = string.Format("http://steamcommunity.com/groups/{0}/memberslistxml/?xml=1", groupname);
var memberslistxml = XDocument.Load(url);
var element = memberslistxml.Element("memberList");
if (element == null) return null;
var xElement = element.Element("members");
if (xElement == null) return null;
var memberslist = xElement.Descendants();
return memberslist.Select(d=>d.Value.ToString(CultureInfo.InvariantCulture));
}
// http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=A13AE8E10434F74C0D3B3D1A86A9AF98&steamid=76561198005179566&include_appinfo=1&format=xml optional: include_played_free_games=1
//<response>
//<game_count>79</game_count>
//<games>
// <message>
// <appid>570</appid>
// <name>Dota 2</name>
// <playtime_2weeks>578</playtime_2weeks>
// <playtime_forever>63771</playtime_forever>
// <img_icon_url>0bbb630d63262dd66d2fdd0f7d37e8661a410075</img_icon_url>
// <img_logo_url>d4f836839254be08d8e9dd333ecc9a01782c26d2</img_logo_url>
// </message>
// <message>
// <appid>80</appid>
// <playtime_forever>1249</playtime_forever>
// </message>
// <message>
// <appid>100</appid>
// <playtime_forever>18</playtime_forever>
// </message>
private static IEnumerable<Game> GetPlayerOwnedGames(string steamplayerid)
{
string url = string.Format("http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key={0}&steamid={1}&include_appinfo=1&format=xml", key, steamplayerid);
var memberslistxml = XDocument.Load(url);
var element = memberslistxml.Element("response");
if (element == null) yield break;
var xElement = element.Element("games");
if (xElement == null) yield break;
foreach (var node in xElement.Descendants("message"))
{
var game = new Game();
var appid = node.Element("appid");
if (appid != null) game.AppId = appid.Value;
var name = node.Element("name");
if (name != null) game.Name = name.Value;
var playtime_2weeks = node.Element("playtime_2weeks");
if (playtime_2weeks != null) game.Playtime2weeks = Convert.ToInt32(playtime_2weeks.Value);
var playtime_forever = node.Element("playtime_forever");
if (playtime_forever != null) game.PlaytimeForever = Convert.ToInt32(playtime_forever.Value);
if (game.AppId != null && game.Name != null && game.PlaytimeForever != 0)
{
yield return game;
}
}
}
// http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=A13AE8E10434F74C0D3B3D1A86A9AF98&steamids=76561197960435530&format=xml
// <response>
// - <players>
// - <player>
// <steamid>76561197960435530</steamid>
// <communityvisibilitystate>3</communityvisibilitystate>
// <profilestate>1</profilestate>
// <personaname>Robin</personaname>
// <lastlogoff>1411922954</lastlogoff>
// <profileurl>http://steamcommunity.com/id/robinwalker/</profileurl>
// <avatar>http://media.steampowered.com/steamcommunity/public/images/avatars/f1/f1dd60a188883caf82d0cbfccfe6aba0af1732d4.jpg</avatar>
// <avatarmedium>http://media.steampowered.com/steamcommunity/public/images/avatars/f1/f1dd60a188883caf82d0cbfccfe6aba0af1732d4_medium.jpg</avatarmedium>
// <avatarfull>http://media.steampowered.com/steamcommunity/public/images/avatars/f1/f1dd60a188883caf82d0cbfccfe6aba0af1732d4_full.jpg</avatarfull>
// <personastate>0</personastate>
// <realname>Robin Walker</realname>
// <primaryclanid>103582791429521412</primaryclanid>
// <timecreated>1063407589</timecreated>
// <personastateflags>0</personastateflags>
// <loccountrycode>US</loccountrycode>
// <locstatecode>WA</locstatecode>
// <loccityid>3961</loccityid>
// </player>
// </players>
//</response>
private static IEnumerable<Player> GetPlayerSummaries(string steamplayerids)
{
string url = string.Format("http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={0}&steamids={1}&format=xml", key, steamplayerids);
var memberslistxml = XDocument.Load(url);
var element = memberslistxml.Element("response");
if (element == null) yield break;
var xElement = element.Element("players");
if (xElement == null) yield break;
foreach (var node in xElement.Descendants("player"))
{
var friend = new Player();
var steamid = node.Element("steamid");
if (steamid != null) friend.SteamId = steamid.Value;
var personaname = node.Element("personaname");
if (personaname != null) friend.PersonaName = personaname.Value;
var realname = node.Element("realname");
if (realname != null) friend.RealName = realname.Value;
var avatar = node.Element("avatar");
if (avatar != null) friend.Avatar = avatar.Value;
if (friend.SteamId != null && friend.PersonaName != null && friend.RealName != null && friend.Avatar != null)
{
yield return friend;
}
}
}
static void Main(string[] args)
{
// Generate Group Memberlist
List<string> memberlist = null;
try
{
memberlist = GetGroupMembers(groupname).ToList();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return;
}
// Map relation to friend in first level
var playerRelations = new List<PlayerRelation>();
Console.WriteLine(string.Format("Found {0} members in steam group {1}", memberlist.Count(), groupname));
foreach (var member in memberlist)
{
Console.WriteLine("Looking up relations for member " + member);
var playerRel = new PlayerRelation(member);
try
{
var friends = GetFriendList(member);
playerRel.Friends = friends.ToList();
Console.WriteLine("Found {0} friends", playerRel.Friends.Count());
}
catch (Exception e)
{
Console.WriteLine("No friends found. Error '{0}'", e.Message);
}
try
{
var games = GetPlayerOwnedGames(member);
playerRel.Games = games.ToList();
Console.WriteLine("Found {0} games", playerRel.Games.Count());
}
catch (Exception e)
{
Console.WriteLine("No games found. Error '{0}'", e.Message);
}
playerRelations.Add(playerRel);
}
Console.WriteLine("Ready.\n\nPress enter to save to disk ");
Console.ReadKey();
// Generate player and friend information
var playerSteamIds = string.Join(",", playerRelations.Select(d => d.SteamId));
var summary = GetPlayerSummaries(playerSteamIds);
foreach (var player in summary)
{
var playerRel = playerRelations.FirstOrDefault(p => p.SteamId == player.SteamId);
if (playerRel == null) continue;
playerRel.RealName = player.RealName;
playerRel.PersonaName = player.PersonaName;
playerRel.Avatar = player.Avatar;
var playerFriendRel = playerRelations.Where(p => p.Friends != null && p.Friends.Any()).SelectMany(p => p.Friends.FindAll(o => o.SteamId == player.SteamId));
foreach (var friend in playerFriendRel)
{
friend.RealName = player.RealName;
friend.PersonaName = player.PersonaName;
friend.Avatar = player.Avatar;
}
}
// Generate output to Flare Format
// We need to generate
// {"name":"flare.analytics.cluster.AgglomerativeCluster","size":3938,"imports":["flare.animate.Transitioner","flare.vis.data.DataList","flare.util.math.IMatrix","flare.analytics.cluster.MergeEdge","flare.analytics.cluster.HierarchicalCluster","flare.vis.data.Data"]}
var outputFlare = new StringBuilder();
bool insertSeparator = false;
foreach (var playerRelation in playerRelations.OrderBy(s=>s.Games.Sum(d=>d.PlaytimeForever)))
{
if (insertSeparator)
outputFlare.Append(",");
else
insertSeparator = true;
var friendlist = string.Empty;
var friendcount = 5;
if (playerRelation.Friends != null && playerRelation.Friends.Any())
{
friendlist = string.Join(",", playerRelation.Friends.Select(f => string.Format("{0}", System.Web.Helpers.Json.Encode(f.SteamId))));
friendcount = playerRelation.Friends.Count*10;
}
var playtime = playerRelation.Games.Sum(d => d.PlaytimeForever) / 60;
outputFlare.AppendFormat("{{\"name\":{0},\"alias\":{1},\"size\":{2},\"imports\":[{3}]}}\n", System.Web.Helpers.Json.Encode(playerRelation.SteamId), System.Web.Helpers.Json.Encode(playerRelation.ToString() + " played " + playtime + "h"), friendcount, friendlist);
}
File.WriteAllText("flare-steamfriendrelations.txt", "[\n" + outputFlare + "]");
Console.WriteLine("Saved friend relations\n\n");
outputFlare = new StringBuilder();
insertSeparator = false;
var gameRepository = new List<Game>();
foreach (var playerRelation in playerRelations.OrderBy(s => s.Games.Sum(d => d.PlaytimeForever)))
{
if (insertSeparator)
outputFlare.Append(",");
else
insertSeparator = true;
var gamelist = string.Empty;
var playtime = 0;
if (playerRelation.Games != null && playerRelation.Games.Any())
{
gamelist = string.Join(",", playerRelation.Games.Select(f => string.Format("{0}", System.Web.Helpers.Json.Encode(f.AppId))));
playerRelation.Games.ForEach(g =>
{
if (!gameRepository.Exists(x => x.AppId == g.AppId))
{
g.TotalGroupPlaytime = g.PlaytimeForever;
gameRepository.Add(g);
}
else
{
gameRepository.Find(x => x.AppId == g.AppId).TotalGroupPlaytime += g.PlaytimeForever;
}
});
playtime = playerRelation.Games.Sum(d => d.PlaytimeForever) / 60;
}
outputFlare.AppendFormat("{{\"name\":{0},\"alias\":{1},\"size\":{2},\"imports\":[{3}]}}\n", System.Web.Helpers.Json.Encode(playerRelation.SteamId), System.Web.Helpers.Json.Encode(playerRelation.ToString() + " played " + playtime + " h"), playtime, gamelist);
}
// list games for allowing link generation
var gamejson = new List<string>();
foreach (var game in gameRepository.OrderByDescending(g => g.TotalGroupPlaytime).Take(120))
{
gamejson.Add(string.Format("{{\"name\":{0},\"alias\":{1},\"size\":{2},\"imports\":[{3}]}}\n", System.Web.Helpers.Json.Encode(game.AppId), System.Web.Helpers.Json.Encode(game.Name + " " + game.TotalGroupPlaytime + " h"), game.TotalGroupPlaytime, string.Empty));
}
outputFlare.Append(",");
outputFlare.Append(string.Join(",", gamejson));
File.WriteAllText("flare-steamgamerelations.txt", "[\n" + outputFlare + "]");
Console.WriteLine("Saved game relations\n\n");
Console.WriteLine("Done.\n\nPress enter to exit");
Console.ReadKey();
}
#endregion Methods
#region Nested Types
public class Game
{
public Game()
{
PlaytimeForever = 0;
TotalGroupPlaytime = 0;
Playtime2weeks = 0;
}
public int Playtime2weeks;
public string AppId { get; set; }
public int PlaytimeForever { get; set; }
public string Name { get; set; }
public int TotalGroupPlaytime { get; set; }
}
public class Friend : Player
{
#region Fields
public int? FriendSince;
public string Relationship;
#endregion Fields
}
public class Player
{
#region Fields
public string Avatar;
public string PersonaName;
public string RealName;
public string SteamId;
#endregion Fields
public override string ToString()
{
if (!string.IsNullOrWhiteSpace(PersonaName) && !string.IsNullOrWhiteSpace(RealName))
{
return string.Format("{0} aka {1}", PersonaName.Trim(), RealName.Trim());
}
else if (!string.IsNullOrWhiteSpace(PersonaName))
{
return string.Format("{0}", PersonaName.Trim());
}
else if (!string.IsNullOrWhiteSpace(RealName))
{
return string.Format("{0}", RealName.Trim());
}
else
{
return SteamId;
}
}
}
private class PlayerRelation : Player
{
#region Fields
#endregion Fields
#region Constructors
public PlayerRelation(string steamId)
{
this.SteamId = steamId;
}
public PlayerRelation(string steamId, List<Friend> friends)
{
this.SteamId = steamId;
Friends = friends;
}
#endregion Constructors
#region Properties
public Player Player { get; set; }
public List<Friend> Friends { get; set; }
public List<Game> Games { get; set; }
#endregion Properties
}
#endregion Nested Types
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment