

{"id":1145,"date":"2012-06-02T18:47:43","date_gmt":"2012-06-02T23:47:43","guid":{"rendered":"http:\/\/rud.is\/b\/?p=1145"},"modified":"2012-06-03T08:24:08","modified_gmt":"2012-06-03T13:24:08","slug":"slopegraphs-in-python-the-great-refactor","status":"publish","type":"post","link":"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/","title":{"rendered":"Slopegraphs in Python \u2013 The Great Refactor"},"content":{"rendered":"<p>Despite being on holiday, I had a spare hour to refactor the code (@mrshrbrmstr was joining the 1% in the hotel spa). It&#8217;s up on <a href=\"https:\/\/github.com\/hrbrmstr\/slopegraph\">github<\/a> and now sports a spiffy JSON-format config file. You now must execute the <code>slopegraph.py<\/code> script with a &#8220;<code>--config FILENAME<\/code>&#8221; argument. The configuration file lets you specify the &#8220;theme&#8221; as well as the input file and output format (you can only use PDF for the moment).<\/p>\n<p>Here&#8217;s a sample config file included in the github push (there&#8217;s another one there too):<\/p>\n<pre lang=\"javascript\" line=\"1\">{\r\n\r\n\"font_family\" : \"Palatino\",\r\n\"font_size\" : \"20\",\r\n\r\n\"x_margin\" : \"20\",\r\n\"y_margin\" : \"30\",\r\n\r\n\"line_width\" : \"0.5\",\r\n\r\n\"background_color\" : \"DEC299\",\r\n\"label_color\" : \"687D64\",\r\n\"value_color\" : \"949258\",\r\n\"slope_color\" : \"61514C\",\r\n\r\n\"value_format_string\" : \"%2d\",\r\n\r\n\"input\" : \"television.csv\",\r\n\"output\" : \"television\",\r\n\"format\" : \"pdf\"\r\n\r\n}<\/pre>\n<p>Included in the refactor is the ability to use a <code>sprintf<\/code>-like format string for the label value output to make the slopegraphs a tad prettier. Also included with the refactor is a new limitation of the CSV file requiring a<\/p>\n<p><code>\"LABEL, VALUE, VALUE\"<\/code><\/p>\n<p>format in preparation for support for multiple columns. As @jayjacobs said to me, it&#8217;s easy to reformat data into the CSV file format, and, he&#8217;s right (as usual).<\/p>\n<p>Plans for the next revision include:<\/p>\n<ul>\n<li>Specifying a transparent background<\/li>\n<li>Specifying PDF|PS|SVG|PNG format output<\/li>\n<li>Allowing for an arbitrary number of columns for the slopegraph<\/li>\n<li>Optional column labels as well as slopepgraph title (with themeing)<\/li>\n<li>Line color change by slope up\/same\/down value (will most likely be pushed out, tho)<\/li>\n<\/ul>\n<p>Here&#8217;s the whole source:<\/p>\n<pre lang=\"python\" line=\"1\">import csv\r\nimport cairo\r\nimport argparse\r\nimport json\r\n\t\r\ndef split(input, size):\r\n\treturn [input[start:start+size] for start in range(0, len(input), size)]\r\n\r\nclass Slopegraph:\r\n\r\n\tSLOPEGRAPH_CANVAS_SIZE = 300\r\n\r\n\tstarts = {} # starting \"points\"\r\n\tends = {} # ending \"points\"\r\n\tpairs = [] # base pair array for the final plotting\r\n\t\r\n\tdef readCSV(self, filename):\r\n\t\r\n\t\tslopeReader = csv.reader(open(filename, 'rb'), delimiter=',', quotechar='\"')\r\n\t\r\n\t\tfor row in slopeReader:\r\n\t\t\r\n\t\t\t# add chosen values (need start\/end for each CSV row) to the final plotting array.\r\n\t\t\t\r\n\t\t\tlab = row[0] # label\r\n\t\t\tbeg = float(row[1]) # left vals\r\n\t\t\tend = float(row[2]) # right vals\r\n\t\t\t\r\n\t\t\tself.pairs.append( (float(beg), float(end)) )\r\n\t\t\r\n\t\t\t# combine labels of common values into one string\r\n\t\t\r\n\t\t\tif beg in self.starts:\r\n\t\t\t\tself.starts[beg] = self.starts[beg] + \"; \" + lab\r\n\t\t\telse:\r\n\t\t\t\tself.starts[beg] = lab\r\n\t\t\t\r\n\t\t\r\n\t\t\tif end in self.ends:\r\n\t\t\t\tself.ends[end] = self.ends[end] + \"; \" + lab\r\n\t\t\telse:\r\n\t\t\t\tself.ends[end] = lab\r\n\r\n\r\n\tdef sortKeys(self):\r\n\t\r\n\t\t# sort all the values (in the event the CSV wasn't) so\r\n\t\t# we can determine the smallest increment we need to use\r\n\t\t# when stacking the labels and plotting points\r\n\t\t\r\n\t\tself.startSorted = [(k, self.starts[k]) for k in sorted(self.starts)]\r\n\t\tself.endSorted = [(k, self.ends[k]) for k in sorted(self.ends)]\r\n\t\t\r\n\t\tself.startKeys = sorted(self.starts.keys())\r\n\t\tself.delta = max(self.startSorted)\r\n\t\tfor i in range(len(self.startKeys)):\r\n\t\t\tif (i+1 <= len(self.startKeys)-1):\r\n\t\t\t\tcurrDelta = float(self.startKeys[i+1]) - float(self.startKeys[i])\r\n\t\t\t\tif (currDelta < self.delta):\r\n\t\t\t\t\tself.delta = currDelta\r\n\t\t\t\t\t\r\n\t\tself.endKeys = sorted(self.ends.keys())\r\n\t\tfor i in range(len(self.endKeys)):\r\n\t\t\tif (i+1 <= len(self.endKeys)-1):\r\n\t\t\t\tcurrDelta = float(self.endKeys[i+1]) - float(self.endKeys[i])\r\n\t\t\t\tif (currDelta < self.delta):\r\n\t\t\t\t\tself.delta = currDelta\r\n\r\n\r\n\tdef findExtremes(self):\r\n\t\r\n\t\t# we also need to find the absolute min &#038; max values\r\n\t\t# so we know how to scale the plots\r\n\t\t\r\n\t\tself.lowest = min(self.startKeys)\r\n\t\tif (min(self.endKeys) < self.lowest) : self.lowest = min(self.endKeys)\r\n\t\t\r\n\t\tself.highest = max(self.startKeys)\r\n\t\tif (max(self.endKeys) > self.highest) : self.highest = max(self.endKeys)\r\n\t\t\r\n\t\tself.delta = float(self.delta)\r\n\t\tself.lowest = float(self.lowest)\r\n\t\tself.highest = float(self.highest)\r\n\r\n\t\r\n\tdef calculateExtents(self, filename, format, valueFormatString):\r\n\t\r\n\t\tsurface = cairo.PDFSurface (filename, 8.5*72, 11*72)\r\n\t\tcr = cairo.Context (surface)\r\n\t\tcr.save()\r\n\t\tcr.select_font_face(self.FONT_FAMILY, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)\r\n\t\tcr.set_font_size(self.FONT_SIZE)\r\n\t\tcr.set_line_width(self.LINE_WIDTH)\r\n\t\t\r\n\t\t# find the *real* maximum label width (not just based on number of chars)\r\n\t\t\r\n\t\tmaxLabelWidth = 0\r\n\t\tmaxNumWidth = 0\r\n\t\t\r\n\t\tfor k in sorted(self.startKeys):\r\n\t\t\ts1 = self.starts[k]\r\n\t\t\txbearing, ybearing, self.sWidth, self.sHeight, xadvance, yadvance = (cr.text_extents(s1))\r\n\t\t\tif (self.sWidth > maxLabelWidth) : maxLabelWidth = self.sWidth\r\n\t\t\txbearing, ybearing, self.startMaxLabelWidth, startMaxLabelHeight, xadvance, yadvance = (cr.text_extents(valueFormatString % (k)))\r\n\t\t\tif (self.startMaxLabelWidth > maxNumWidth) : maxNumWidth = self.startMaxLabelWidth\r\n\t\t\r\n\t\tself.sWidth = maxLabelWidth\r\n\t\tself.startMaxLabelWidth = maxNumWidth\r\n\t\t\r\n\t\tmaxLabelWidth = 0\r\n\t\tmaxNumWidth = 0\r\n\t\t\r\n\t\tfor k in sorted(self.endKeys):\r\n\t\t\te1 = self.ends[k]\r\n\t\t\txbearing, ybearing, self.eWidth, eHeight, xadvance, yadvance = (cr.text_extents(e1))\r\n\t\t\tif (self.eWidth > maxLabelWidth) : maxLabelWidth = self.eWidth\r\n\t\t\txbearing, ybearing, self.endMaxLabelWidth, endMaxLabelHeight, xadvance, yadvance = (cr.text_extents(valueFormatString % (k)))\r\n\t\t\tif (self.endMaxLabelWidth > maxNumWidth) : maxNumWidth = self.endMaxLabelWidth\r\n\t\t\r\n\t\tself.eWidth = maxLabelWidth\r\n\t\tself.endMaxLabelWidth = maxNumWidth\t\r\n\t\t\r\n\t\tcr.restore()\r\n\t\tcr.show_page()\r\n\t\tsurface.finish()\r\n\t\t\r\n\t\tself.width = self.X_MARGIN + self.sWidth + self.SPACE_WIDTH + self.startMaxLabelWidth + self.SPACE_WIDTH + self.SLOPEGRAPH_CANVAS_SIZE + self.SPACE_WIDTH + self.endMaxLabelWidth + self.SPACE_WIDTH + self.eWidth + self.X_MARGIN ;\r\n\t\tself.height = (self.Y_MARGIN * 2) + (((self.highest - self.lowest) \/ self.delta) * self.LINE_HEIGHT)\r\n\t\t\r\n\t\t\r\n\tdef makeSlopegraph(self, filename, config):\r\n\t\r\n\t\t(lab_r,lab_g,lab_b) = split(config[\"label_color\"],2)\r\n\t\t(val_r,val_g,val_b) = split(config[\"value_color\"],2)\r\n\t\t(line_r,line_g,line_b) = split(config[\"slope_color\"],2)\r\n\t\t(bg_r,bg_g,bg_b) = split(config[\"background_color\"],2)\r\n\t\t\r\n\t\tLAB_R = (int(lab_r, 16)\/255.0)\r\n\t\tLAB_G = (int(lab_g, 16)\/255.0)\r\n\t\tLAB_B = (int(lab_b, 16)\/255.0)\r\n\t\t\r\n\t\tVAL_R = (int(val_r, 16)\/255.0)\r\n\t\tVAL_G = (int(val_g, 16)\/255.0)\r\n\t\tVAL_B = (int(val_b, 16)\/255.0)\r\n\t\t\r\n\t\tLINE_R = (int(line_r, 16)\/255.0)\r\n\t\tLINE_G = (int(line_g, 16)\/255.0)\r\n\t\tLINE_B = (int(line_b, 16)\/255.0)\r\n\t\t\r\n\t\tBG_R = (int(bg_r, 16)\/255.0)\r\n\t\tBG_G = (int(bg_g, 16)\/255.0)\r\n\t\tBG_B = (int(bg_b, 16)\/255.0)\r\n\r\n\t\tsurface = cairo.PDFSurface (filename, self.width, self.height)\r\n\t\tcr = cairo.Context(surface)\r\n\t\t\r\n\t\tcr.save()\r\n\t\t\r\n\t\tcr.select_font_face(self.FONT_FAMILY, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)\r\n\t\tcr.set_font_size(self.FONT_SIZE)\r\n\t\t\r\n\t\tcr.set_line_width(self.LINE_WIDTH)\r\n\t\t\r\n\t\tcr.set_source_rgb(BG_R,BG_G,BG_B)\r\n\t\tcr.rectangle(0,0,self.width,self.height)\r\n\t\tcr.fill()\r\n\t\t\r\n\t\t# draw start labels at the correct positions\r\n\t\t\r\n\t\tvalueFormatString = config[\"value_format_string\"]\r\n\t\t\r\n\t\tfor k in sorted(self.startKeys):\r\n\t\t\r\n\t\t\tval = float(k)\r\n\t\t\tlabel = self.starts[k]\r\n\t\t\txbearing, ybearing, lWidth, lHeight, xadvance, yadvance = (cr.text_extents(label))\r\n\t\t\txbearing, ybearing, kWidth, kHeight, xadvance, yadvance = (cr.text_extents(valueFormatString % (val)))\r\n\t\t\r\n\t\t\tcr.set_source_rgb(LAB_R,LAB_G,LAB_B)\r\n\t\t\tcr.move_to(self.X_MARGIN + (self.sWidth - lWidth), self.Y_MARGIN + (self.highest - val) * self.LINE_HEIGHT * (1\/self.delta))\r\n\t\t\tcr.show_text(label)\r\n\t\t\t\r\n\t\t\tcr.set_source_rgb(VAL_R,VAL_G,VAL_B)\r\n\t\t\tcr.move_to(self.X_MARGIN + self.sWidth + self.SPACE_WIDTH + (self.startMaxLabelWidth - kWidth), self.Y_MARGIN + (self.highest - val) * self.LINE_HEIGHT * (1\/self.delta))\r\n\t\t\tcr.show_text(valueFormatString % (val))\r\n\t\t\t\r\n\t\t\tcr.stroke()\r\n\t\t\r\n\t\t# draw end labels at the correct positions\r\n\t\t\r\n\t\tfor k in sorted(self.endKeys):\r\n\t\t\r\n\t\t\tval = float(k)\r\n\t\t\tlabel = self.ends[k]\r\n\t\t\txbearing, ybearing, lWidth, lHeight, xadvance, yadvance = (cr.text_extents(label))\r\n\t\t\t\t\r\n\t\t\tcr.set_source_rgb(VAL_R,VAL_G,VAL_B)\r\n\t\t\tcr.move_to(self.width - self.X_MARGIN - self.SPACE_WIDTH - self.eWidth - self.SPACE_WIDTH - self.endMaxLabelWidth, self.Y_MARGIN + (self.highest - val) * self.LINE_HEIGHT * (1\/self.delta))\r\n\t\t\tcr.show_text(valueFormatString % (val))\r\n\t\t\r\n\t\t\tcr.set_source_rgb(LAB_R,LAB_G,LAB_B)\r\n\t\t\tcr.move_to(self.width - self.X_MARGIN - self.SPACE_WIDTH - self.eWidth, self.Y_MARGIN + (self.highest - val) * self.LINE_HEIGHT * (1\/self.delta))\r\n\t\t\tcr.show_text(label)\r\n\t\t\r\n\t\t\tcr.stroke()\r\n\t\t\r\n\t\t# do the actual plotting\r\n\t\t\r\n\t\tcr.set_line_width(self.LINE_WIDTH)\r\n\t\tcr.set_source_rgb(LINE_R, LINE_G, LINE_B)\r\n\t\t\r\n\t\tfor s1,e1 in self.pairs:\r\n\t\t\tcr.move_to(self.X_MARGIN + self.sWidth + self.SPACE_WIDTH + self.startMaxLabelWidth + self.LINE_START_DELTA, self.Y_MARGIN + (self.highest - s1) * self.LINE_HEIGHT * (1\/self.delta) - self.LINE_HEIGHT\/4)\r\n\t\t\tcr.line_to(self.width - self.X_MARGIN - self.eWidth - self.SPACE_WIDTH - self.endMaxLabelWidth - self.LINE_START_DELTA, self.Y_MARGIN + (self.highest - e1) * self.LINE_HEIGHT * (1\/self.delta) - self.LINE_HEIGHT\/4)\r\n\t\t\tcr.stroke()\r\n\t\t\r\n\t\tcr.restore()\r\n\t\tcr.show_page()\r\n\t\tsurface.finish()\t\r\n\t\t\r\n\t\r\n\tdef __init__(self, config):\r\n\t\r\n\t\t# a couple methods need these so make them local to the class\r\n\t\r\n\t\tself.FONT_FAMILY = config[\"font_family\"]\r\n\t\tself.LINE_WIDTH = float(config[\"line_width\"])\r\n\t\tself.X_MARGIN = float(config[\"x_margin\"])\r\n\t\tself.Y_MARGIN = float(config[\"y_margin\"])\r\n\t\tself.FONT_SIZE = float(config[\"font_size\"])\r\n\t\tself.SPACE_WIDTH = self.FONT_SIZE \/ 2.0\r\n\t\tself.LINE_HEIGHT = self.FONT_SIZE + (self.FONT_SIZE \/ 2.0)\r\n\t\tself.LINE_START_DELTA = 1.5*self.SPACE_WIDTH\r\n\t\t\r\n\t\tOUTPUT_FILE = config[\"output\"] + \".\" + config[\"format\"]\r\n\t\t\r\n\t\t# process the values & make the slopegraph\r\n\t\t\r\n\t\tself.readCSV(config[\"input\"])\r\n\t\tself.sortKeys()\r\n\t\tself.findExtremes()\r\n\t\tself.calculateExtents(OUTPUT_FILE, config[\"format\"], config[\"value_format_string\"])\r\n\t\tself.makeSlopegraph(OUTPUT_FILE, config)\r\n\t\t\r\n\r\ndef main():\r\n\r\n\tparser = argparse.ArgumentParser(description=\"Creates a slopegraph from a CSV source\")\r\n\tparser.add_argument(\"--config\",required=True,\r\n\t\t\t\t\thelp=\"config file name to use for  slopegraph creation\",)\r\n\targs = parser.parse_args()\r\n\r\n\tif args.config:\r\n\t\r\n\t\tjson_data = open(args.config)\r\n\t\tconfig = json.load(json_data)\r\n\t\tjson_data.close()\r\n\t\t\r\n\t\tSlopegraph(config)\r\n\r\n\treturn(0)\r\n\t\r\nif __name__ == \"__main__\":\r\n\tmain()<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Despite being on holiday, I had a spare hour to refactor the code (@mrshrbrmstr was joining the 1% in the hotel spa). It&#8217;s up on github and now sports a spiffy JSON-format config file. You now must execute the slopegraph.py script with a &#8220;&#8211;config FILENAME&#8221; argument. The configuration file lets you specify the &#8220;theme&#8221; as [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"activitypub_content_warning":"","activitypub_content_visibility":"","activitypub_max_image_attachments":3,"activitypub_interaction_policy_quote":"anyone","activitypub_status":"","footnotes":""},"categories":[24,63,640],"tags":[751,657,658],"class_list":["post-1145","post","type-post","status-publish","format-standard","hentry","category-charts-graphs","category-development","category-python-2","tag-slopegraph","tag-table-chart","tag-tufte"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.2 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Slopegraphs in Python \u2013 The Great Refactor - rud.is<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Slopegraphs in Python \u2013 The Great Refactor - rud.is\" \/>\n<meta property=\"og:description\" content=\"Despite being on holiday, I had a spare hour to refactor the code (@mrshrbrmstr was joining the 1% in the hotel spa). It&#8217;s up on github and now sports a spiffy JSON-format config file. You now must execute the slopegraph.py script with a &#8220;--config FILENAME&#8221; argument. The configuration file lets you specify the &#8220;theme&#8221; as [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/\" \/>\n<meta property=\"og:site_name\" content=\"rud.is\" \/>\n<meta property=\"article:published_time\" content=\"2012-06-02T23:47:43+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2012-06-03T13:24:08+00:00\" \/>\n<meta name=\"author\" content=\"hrbrmstr\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"hrbrmstr\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/\"},\"author\":{\"name\":\"hrbrmstr\",\"@id\":\"https:\/\/rud.is\/b\/#\/schema\/person\/d7cb7487ab0527447f7fda5c423ff886\"},\"headline\":\"Slopegraphs in Python \u2013 The Great Refactor\",\"datePublished\":\"2012-06-02T23:47:43+00:00\",\"dateModified\":\"2012-06-03T13:24:08+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/\"},\"wordCount\":219,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/rud.is\/b\/#\/schema\/person\/d7cb7487ab0527447f7fda5c423ff886\"},\"keywords\":[\"slopegraph\",\"table-chart\",\"tufte\"],\"articleSection\":[\"Charts &amp; Graphs\",\"Development\",\"Python\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/\",\"url\":\"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/\",\"name\":\"Slopegraphs in Python \u2013 The Great Refactor - rud.is\",\"isPartOf\":{\"@id\":\"https:\/\/rud.is\/b\/#website\"},\"datePublished\":\"2012-06-02T23:47:43+00:00\",\"dateModified\":\"2012-06-03T13:24:08+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/rud.is\/b\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Slopegraphs in Python \u2013 The Great Refactor\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/rud.is\/b\/#website\",\"url\":\"https:\/\/rud.is\/b\/\",\"name\":\"rud.is\",\"description\":\"&quot;In God we trust. All others must bring data&quot;\",\"publisher\":{\"@id\":\"https:\/\/rud.is\/b\/#\/schema\/person\/d7cb7487ab0527447f7fda5c423ff886\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/rud.is\/b\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\/\/rud.is\/b\/#\/schema\/person\/d7cb7487ab0527447f7fda5c423ff886\",\"name\":\"hrbrmstr\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/i0.wp.com\/rud.is\/b\/wp-content\/uploads\/2023\/10\/ukr-shield.png?fit=460%2C460&ssl=1\",\"url\":\"https:\/\/i0.wp.com\/rud.is\/b\/wp-content\/uploads\/2023\/10\/ukr-shield.png?fit=460%2C460&ssl=1\",\"contentUrl\":\"https:\/\/i0.wp.com\/rud.is\/b\/wp-content\/uploads\/2023\/10\/ukr-shield.png?fit=460%2C460&ssl=1\",\"width\":460,\"height\":460,\"caption\":\"hrbrmstr\"},\"logo\":{\"@id\":\"https:\/\/i0.wp.com\/rud.is\/b\/wp-content\/uploads\/2023\/10\/ukr-shield.png?fit=460%2C460&ssl=1\"},\"description\":\"Don't look at me\u2026I do what he does \u2014 just slower. #rstats avuncular \u2022 ?Resistance Fighter \u2022 Cook \u2022 Christian \u2022 [Master] Chef des Donn\u00e9es de S\u00e9curit\u00e9 @ @rapid7\",\"sameAs\":[\"http:\/\/rud.is\"],\"url\":\"https:\/\/rud.is\/b\/author\/hrbrmstr\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Slopegraphs in Python \u2013 The Great Refactor - rud.is","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/","og_locale":"en_US","og_type":"article","og_title":"Slopegraphs in Python \u2013 The Great Refactor - rud.is","og_description":"Despite being on holiday, I had a spare hour to refactor the code (@mrshrbrmstr was joining the 1% in the hotel spa). It&#8217;s up on github and now sports a spiffy JSON-format config file. You now must execute the slopegraph.py script with a &#8220;--config FILENAME&#8221; argument. The configuration file lets you specify the &#8220;theme&#8221; as [&hellip;]","og_url":"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/","og_site_name":"rud.is","article_published_time":"2012-06-02T23:47:43+00:00","article_modified_time":"2012-06-03T13:24:08+00:00","author":"hrbrmstr","twitter_card":"summary_large_image","twitter_misc":{"Written by":"hrbrmstr","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/#article","isPartOf":{"@id":"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/"},"author":{"name":"hrbrmstr","@id":"https:\/\/rud.is\/b\/#\/schema\/person\/d7cb7487ab0527447f7fda5c423ff886"},"headline":"Slopegraphs in Python \u2013 The Great Refactor","datePublished":"2012-06-02T23:47:43+00:00","dateModified":"2012-06-03T13:24:08+00:00","mainEntityOfPage":{"@id":"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/"},"wordCount":219,"commentCount":0,"publisher":{"@id":"https:\/\/rud.is\/b\/#\/schema\/person\/d7cb7487ab0527447f7fda5c423ff886"},"keywords":["slopegraph","table-chart","tufte"],"articleSection":["Charts &amp; Graphs","Development","Python"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/","url":"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/","name":"Slopegraphs in Python \u2013 The Great Refactor - rud.is","isPartOf":{"@id":"https:\/\/rud.is\/b\/#website"},"datePublished":"2012-06-02T23:47:43+00:00","dateModified":"2012-06-03T13:24:08+00:00","breadcrumb":{"@id":"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/rud.is\/b\/2012\/06\/02\/slopegraphs-in-python-the-great-refactor\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/rud.is\/b\/"},{"@type":"ListItem","position":2,"name":"Slopegraphs in Python \u2013 The Great Refactor"}]},{"@type":"WebSite","@id":"https:\/\/rud.is\/b\/#website","url":"https:\/\/rud.is\/b\/","name":"rud.is","description":"&quot;In God we trust. All others must bring data&quot;","publisher":{"@id":"https:\/\/rud.is\/b\/#\/schema\/person\/d7cb7487ab0527447f7fda5c423ff886"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/rud.is\/b\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/rud.is\/b\/#\/schema\/person\/d7cb7487ab0527447f7fda5c423ff886","name":"hrbrmstr","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/i0.wp.com\/rud.is\/b\/wp-content\/uploads\/2023\/10\/ukr-shield.png?fit=460%2C460&ssl=1","url":"https:\/\/i0.wp.com\/rud.is\/b\/wp-content\/uploads\/2023\/10\/ukr-shield.png?fit=460%2C460&ssl=1","contentUrl":"https:\/\/i0.wp.com\/rud.is\/b\/wp-content\/uploads\/2023\/10\/ukr-shield.png?fit=460%2C460&ssl=1","width":460,"height":460,"caption":"hrbrmstr"},"logo":{"@id":"https:\/\/i0.wp.com\/rud.is\/b\/wp-content\/uploads\/2023\/10\/ukr-shield.png?fit=460%2C460&ssl=1"},"description":"Don't look at me\u2026I do what he does \u2014 just slower. #rstats avuncular \u2022 ?Resistance Fighter \u2022 Cook \u2022 Christian \u2022 [Master] Chef des Donn\u00e9es de S\u00e9curit\u00e9 @ @rapid7","sameAs":["http:\/\/rud.is"],"url":"https:\/\/rud.is\/b\/author\/hrbrmstr\/"}]}},"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/p23idr-it","jetpack_likes_enabled":true,"jetpack-related-posts":[{"id":1174,"url":"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/","url_meta":{"origin":1145,"position":0},"title":"Slopegraphs in Python \u2013 More Output Tweaks","author":"hrbrmstr","date":"2012-06-05","format":false,"excerpt":"The best way to explain this release will be to walk you through an updated configuration file: { \"label_font_family\" : \"Palatino\", \"label_font_size\" : \"9\", \"header_font_family\" : \"Palatino\", \"header_font_size\" : \"10\", \"x_margin\" : \"20\", \"y_margin\" : \"30\", \"line_width\" : \"0.5\", \"slope_length\" : \"150\", \"labels\" : [ \"1970\", \"1979\" ], \"header_color\" :\u2026","rel":"","context":"In &quot;Charts &amp; Graphs&quot;","block_context":{"text":"Charts &amp; Graphs","link":"https:\/\/rud.is\/b\/category\/charts-graphs\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":1089,"url":"https:\/\/rud.is\/b\/2012\/05\/28\/slopegraphs-in-python\/","url_meta":{"origin":1145,"position":1},"title":"Slopegraphs in Python","author":"hrbrmstr","date":"2012-05-28","format":false,"excerpt":"(NOTE: You can keep up with progress best at github, but can always search on \"slopegraph\" here or just hit the tag page: \"slopegraph\" regularly) I've been a bit obsessed with slopegraphs (a.k.a \"Tufte table-chart\") of late and very dissatisfied with the lack of tools to make this particular visualization\u2026","rel":"","context":"In &quot;Charts &amp; Graphs&quot;","block_context":{"text":"Charts &amp; Graphs","link":"https:\/\/rud.is\/b\/category\/charts-graphs\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":2618,"url":"https:\/\/rud.is\/b\/2013\/09\/04\/nizdos\/","url_meta":{"origin":1145,"position":2},"title":"nizdos &#8211; Nest Thermometer Notifications And Data Logging In Python","author":"hrbrmstr","date":"2013-09-04","format":false,"excerpt":"I've had a Nest thermometer for a while now and it's been an overall positive experience. It's given me more visibility into our heating\/cooling system usage, patterns and preferences; plus, it actually saved us money last winter. We try to avoid running the A\/C during the summer, and it would\u2026","rel":"","context":"In &quot;Development&quot;","block_context":{"text":"Development","link":"https:\/\/rud.is\/b\/category\/development\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":512,"url":"https:\/\/rud.is\/b\/2011\/04\/17\/a-fully-operational-os-x-dbclone\/","url_meta":{"origin":1145,"position":3},"title":"A Fully Operational OS X dbClone","author":"hrbrmstr","date":"2011-04-17","format":false,"excerpt":"Spent some time today updating the missing bits of the OS X version of the Dropbox cloner I uploaded last night. You can just grab the executable or grab the whole project from the github repository. The app can now backup\/restore of local config, clone dropbox configs to a URL\/file\u2026","rel":"","context":"In &quot;Development&quot;","block_context":{"text":"Development","link":"https:\/\/rud.is\/b\/category\/development\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":3386,"url":"https:\/\/rud.is\/b\/2015\/05\/09\/quotebox-an-npr-like-embedded-twitter-quote-generator\/","url_meta":{"origin":1145,"position":4},"title":"quotebox &#8211; An NPR-like Embedded Twitter Quote Generator","author":"hrbrmstr","date":"2015-05-09","format":false,"excerpt":"I'm an avid NPR listener also follow a number of their programs and people on Twitter. I really dig their [quotable](https:\/\/github.com\/nprapps\/quotable) tweets. Here's a sample of a recent one: Minn. state senators cannot look other senators in the eye during floor debate. @ailsachang http:\/\/t.co\/SfQBq4yyHQ pic.twitter.com\/DNHGEiVA9j\u2014 NPR News (@nprnews) May 8,\u2026","rel":"","context":"In &quot;phantomjs&quot;","block_context":{"text":"phantomjs","link":"https:\/\/rud.is\/b\/category\/phantomjs\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":1730,"url":"https:\/\/rud.is\/b\/2012\/10\/24\/extracting-ose-firewall-alert-data-from-imap-gmail-mail-to-csv\/","url_meta":{"origin":1145,"position":5},"title":"Extracting OSE Firewall Alert Data From IMAP (Gmail) Mail To CSV With Python","author":"hrbrmstr","date":"2012-10-24","format":false,"excerpt":"I played around with OSE Firewall for WordPress for a couple days to see if it was worth switching to from the plugin I was previously using. It's definitely not as full featured and I didn't see any WP database extensions where it kept a log I could review\/analyze, so\u2026","rel":"","context":"In &quot;Development&quot;","block_context":{"text":"Development","link":"https:\/\/rud.is\/b\/category\/development\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]}],"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/posts\/1145","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/comments?post=1145"}],"version-history":[{"count":0,"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/posts\/1145\/revisions"}],"wp:attachment":[{"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/media?parent=1145"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/categories?post=1145"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/tags?post=1145"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}