

{"id":1174,"date":"2012-06-05T12:12:53","date_gmt":"2012-06-05T17:12:53","guid":{"rendered":"http:\/\/rud.is\/b\/?p=1174"},"modified":"2018-03-10T07:53:38","modified_gmt":"2018-03-10T12:53:38","slug":"slopegraphs-in-python-more-output-tweaks","status":"publish","type":"post","link":"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/","title":{"rendered":"Slopegraphs in Python \u2013 More Output Tweaks"},"content":{"rendered":"<p>The best way to explain this release will be to walk you through an updated configuration file:<\/p>\n<pre lang=\"javascript\" line=\"1\">{\n\n\"label_font_family\" : \"Palatino\",\n\"label_font_size\" : \"9\",\n\n\"header_font_family\" : \"Palatino\",\n\"header_font_size\" : \"10\",\n\n\"x_margin\" : \"20\",\n\"y_margin\" : \"30\",\n\n\"line_width\" : \"0.5\",\n\n\"slope_length\" : \"150\",\n\n\"labels\" : [ \"1970\", \"1979\" ],\n\n\"header_color\" : \"000000\",\n\"background_color\" : \"FFFFFF\",\n\"label_color\" : \"111111\",\n\"value_color\" : \"999999\",\n\"slope_color\" : \"AAAAAA\",\n\n\"value_format_string\" : \"%2d\",\n\n\"input\" : \"receipts.csv\",\n\"output\" : \"receipts\",\n\"format\" : \"svg\",\n\n\"description\" : \"Current Receipts of Government as a Percentage of Gross Domestic Product, 1970 & 1979\",\n\"source\" : \"Tufte, Edward. The Visual Display of Quantitative Information. Cheshire, Connecticut: Graphics Press; 1983; p. 158\"\n\n}<\/pre>\n<p>I added the ability to include column headers and separated the font specifications for both the column data\/labels and the headers (Lines 2-7). You&#8217;re not required to use headers, so just leave out the header font specification and the &#8220;<code>labels<\/code>&#8221; option (Line 16) if you don&#8217;t want them (it keys off of the font spec, tho). You can also color headers via the &#8220;<code>header_color<\/code>&#8221; option (line 18).<\/p>\n<p>If you use the keyword &#8220;<code>transparent<\/code>&#8221; for the &#8220;<code>background_color<\/code>&#8221; config option (Line 19, tho it&#8217;s not transparent in this example) it will leave out the fill, which is useful for blog posts or embedding in other documents. Works best for PNG &amp; PDF output.<\/p>\n<p>If you want to use a different value for width of the space for the slopelines, you can tweak this via the &#8220;<code>slope_length<\/code>&#8221; option (Line 14). This is setting the stage for multi-column slopegraphs.<\/p>\n<p>When exchanging some communications with @jayjacobs regarding slopegraphs and seeing his <a href=\"https:\/\/www.verizonenterprise.com\/verizon-insights-lab\/?t=securityblog\">spiffy use of them for incident data anlysis<\/a>, it became readily apparent that I needed to include a way of formatting the column data values, so there&#8217;s a &#8220;<code>value_format_string<\/code>&#8221; option, now, that works with Pythonic <code>sprintf<\/code> formats.<\/p>\n<p>Finally, I added &#8220;<code>description<\/code>&#8221; and &#8220;<code>source<\/code>&#8221; options that the code does not yet process, but allows for documenting the configuration a bit, since there&#8217;s no good way to embed comments in a JSON-format configuration file.<\/p>\n<p>As always, the code&#8217;s up on <a href=\"https:\/\/github.com\/hrbrmstr\/slopegraph\">github<\/a> and also below:<\/p>\n<pre lang=\"python\" line=\"1\">import csv\nimport cairo\nimport argparse\nimport json\n    \ndef split(input, size):\n    return [input[start:start+size] for start in range(0, len(input), size)]\n\nclass Slopegraph:\n\n    starts = {} # starting \"points\"\n    ends = {} # ending \"points\"\n    pairs = [] # base pair array for the final plotting\n    \n    def readCSV(self, filename):\n    \n        slopeReader = csv.reader(open(filename, 'rb'), delimiter=',', quotechar='\"')\n    \n        for row in slopeReader:\n        \n            # add chosen values (need start\/end for each CSV row) to the final plotting array.\n            \n            lab = row[0] # label\n            beg = float(row[1]) # left vals\n            end = float(row[2]) # right vals\n            \n            self.pairs.append( (float(beg), float(end)) )\n        \n            # combine labels of common values into one string\n        \n            if beg in self.starts:\n                self.starts[beg] = self.starts[beg] + \"; \" + lab\n            else:\n                self.starts[beg] = lab\n            \n        \n            if end in self.ends:\n                self.ends[end] = self.ends[end] + \"; \" + lab\n            else:\n                self.ends[end] = lab\n\n\n    def sortKeys(self):\n    \n        # sort all the values (in the event the CSV wasn't) so\n        # we can determine the smallest increment we need to use\n        # when stacking the labels and plotting points\n        \n        self.startSorted = [(k, self.starts[k]) for k in sorted(self.starts)]\n        self.endSorted = [(k, self.ends[k]) for k in sorted(self.ends)]\n        \n        self.startKeys = sorted(self.starts.keys())\n        self.delta = max(self.startSorted)\n        for i in range(len(self.startKeys)):\n            if (i+1 <= len(self.startKeys)-1):\n                currDelta = float(self.startKeys[i+1]) - float(self.startKeys[i])\n                if (currDelta < self.delta):\n                    self.delta = currDelta\n                    \n        self.endKeys = sorted(self.ends.keys())\n        for i in range(len(self.endKeys)):\n            if (i+1 <= len(self.endKeys)-1):\n                currDelta = float(self.endKeys[i+1]) - float(self.endKeys[i])\n                if (currDelta < self.delta):\n                    self.delta = currDelta\n\n\n    def findExtremes(self):\n    \n        # we also need to find the absolute min &#038; max values\n        # so we know how to scale the plots\n        \n        self.lowest = min(self.startKeys)\n        if (min(self.endKeys) < self.lowest) : self.lowest = min(self.endKeys)\n        \n        self.highest = max(self.startKeys)\n        if (max(self.endKeys) > self.highest) : self.highest = max(self.endKeys)\n        \n        self.delta = float(self.delta)\n        self.lowest = float(self.lowest)\n        self.highest = float(self.highest)\n\n    \n    def calculateExtents(self, filename, format, valueFormatString):\n    \n        if (format == \"pdf\"):\n            surface = cairo.PDFSurface (filename, 8.5*72, 11*72)\n        elif (format == \"ps\"):\n            surface = cairo.PSSurface(filename, 8.5*72, 11*72)\n            surface.set_eps(True)\n        elif (format == \"svg\"):\n            surface = cairo.SVGSurface (filename, 8.5*72, 11*72)\n        elif (format == \"png\"):\n            surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, int(8.5*72), int(11*72))\n        else:\n            surface = cairo.PDFSurface (filename, 8.5*72, 11*72)\n\n        cr = cairo.Context(surface)\n        cr.save()\n        cr.select_font_face(self.LABEL_FONT_FAMILY, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)\n        cr.set_font_size(self.LABEL_FONT_SIZE)\n        cr.set_line_width(self.LINE_WIDTH)\n        \n        # find the *real* maximum label width (not just based on number of chars)\n        \n        maxLabelWidth = 0\n        maxNumWidth = 0\n        \n        for k in sorted(self.startKeys):\n            s1 = self.starts[k]\n            xbearing, ybearing, self.sWidth, self.sHeight, xadvance, yadvance = (cr.text_extents(s1))\n            if (self.sWidth > maxLabelWidth) : maxLabelWidth = self.sWidth\n            xbearing, ybearing, self.startMaxLabelWidth, startMaxLabelHeight, xadvance, yadvance = (cr.text_extents(valueFormatString % (k)))\n            if (self.startMaxLabelWidth > maxNumWidth) : maxNumWidth = self.startMaxLabelWidth\n        \n        self.sWidth = maxLabelWidth\n        self.startMaxLabelWidth = maxNumWidth\n        \n        maxLabelWidth = 0\n        maxNumWidth = 0\n        \n        for k in sorted(self.endKeys):\n            e1 = self.ends[k]\n            xbearing, ybearing, self.eWidth, eHeight, xadvance, yadvance = (cr.text_extents(e1))\n            if (self.eWidth > maxLabelWidth) : maxLabelWidth = self.eWidth\n            xbearing, ybearing, self.endMaxLabelWidth, endMaxLabelHeight, xadvance, yadvance = (cr.text_extents(valueFormatString % (k)))\n            if (self.endMaxLabelWidth > maxNumWidth) : maxNumWidth = self.endMaxLabelWidth\n        \n        self.eWidth = maxLabelWidth\n        self.endMaxLabelWidth = maxNumWidth \n        \n        cr.restore()\n        cr.show_page()\n        surface.finish()\n        \n        self.width = self.X_MARGIN + self.sWidth + self.SPACE_WIDTH + self.startMaxLabelWidth + self.SPACE_WIDTH + self.SLOPE_LENGTH + self.SPACE_WIDTH + self.endMaxLabelWidth + self.SPACE_WIDTH + self.eWidth + self.X_MARGIN ;\n        self.height = (self.Y_MARGIN * 2) + (((self.highest - self.lowest) \/ self.delta) * self.LINE_HEIGHT)\n        \n        self.HEADER_SPACE = 0.0\n        if (self.HEADER_FONT_FAMILY != None):\n            self.HEADER_SPACE = self.HEADER_FONT_SIZE + 2*self.LINE_HEIGHT\n            self.height += self.HEADER_SPACE\n        \n        \n    def makeSlopegraph(self, filename, config):\n    \n        (lab_r,lab_g,lab_b) = split(config[\"label_color\"],2)        \n        LAB_R = (int(lab_r, 16)\/255.0)\n        LAB_G = (int(lab_g, 16)\/255.0)\n        LAB_B = (int(lab_b, 16)\/255.0)\n        \n        (val_r,val_g,val_b) = split(config[\"value_color\"],2)\n        VAL_R = (int(val_r, 16)\/255.0)\n        VAL_G = (int(val_g, 16)\/255.0)\n        VAL_B = (int(val_b, 16)\/255.0)\n        \n        (line_r,line_g,line_b) = split(config[\"slope_color\"],2)\n        LINE_R = (int(line_r, 16)\/255.0)\n        LINE_G = (int(line_g, 16)\/255.0)\n        LINE_B = (int(line_b, 16)\/255.0)\n        \n        if (config[\"background_color\"] != \"transparent\"):\n            (bg_r,bg_g,bg_b) = split(config[\"background_color\"],2)\n            BG_R = (int(bg_r, 16)\/255.0)\n            BG_G = (int(bg_g, 16)\/255.0)\n            BG_B = (int(bg_b, 16)\/255.0)\n    \n        if (config['format'] == \"pdf\"):\n            surface = cairo.PDFSurface (filename, self.width, self.height)\n        elif (config['format'] == \"ps\"):\n            surface = cairo.PSSurface(filename, self.width, self.height)\n            surface.set_eps(True)\n        elif (config['format'] == \"svg\"):\n            surface = cairo.SVGSurface (filename, self.width, self.height)\n        elif (config['format'] == \"png\"):\n            surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, int(self.width), int(self.height))\n        else:\n            surface = cairo.PDFSurface (filename, self.width, self.height)\n            \n        cr = cairo.Context(surface)\n        \n        cr.save()\n        \n        cr.set_line_width(self.LINE_WIDTH)\n\n        if (config[\"background_color\"] != \"transparent\"):\n            cr.set_source_rgb(BG_R,BG_G,BG_B)\n            cr.rectangle(0,0,self.width,self.height)\n            cr.fill()\n            \n        # draw headers (if present)\n        \n        if (self.HEADER_FONT_FAMILY != None):\n            \n            (header_r,header_g,header_b) = split(config[\"header_color\"],2)      \n            HEADER_R = (int(header_r, 16)\/255.0)\n            HEADER_G = (int(header_g, 16)\/255.0)\n            HEADER_B = (int(header_b, 16)\/255.0)\n            \n            cr.save()\n            \n            cr.select_font_face(self.HEADER_FONT_FAMILY, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)\n            cr.set_font_size(self.HEADER_FONT_SIZE)\n            cr.set_source_rgb(HEADER_R,HEADER_G,HEADER_B)\n            \n            xbearing, ybearing, hWidth, hHeight, xadvance, yadvance = (cr.text_extents(config[\"labels\"][0]))            \n            cr.move_to(self.X_MARGIN + self.sWidth - hWidth, self.Y_MARGIN + self.HEADER_FONT_SIZE)\n            cr.show_text(config[\"labels\"][0])\n            \n            xbearing, ybearing, hWidth, hHeight, xadvance, yadvance = (cr.text_extents(config[\"labels\"][1]))            \n            cr.move_to(self.width - self.X_MARGIN - self.SPACE_WIDTH - self.eWidth, self.Y_MARGIN + self.HEADER_FONT_SIZE)\n            cr.show_text(config[\"labels\"][1])\n    \n            cr.stroke()\n    \n            cr.restore()\n                \n        # draw start labels at the correct positions\n        \n        cr.select_font_face(self.LABEL_FONT_FAMILY, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)\n        cr.set_font_size(self.LABEL_FONT_SIZE)\n        \n        valueFormatString = config[\"value_format_string\"]\n        \n        for k in sorted(self.startKeys):\n        \n            val = float(k)\n            label = self.starts[k]\n            xbearing, ybearing, lWidth, lHeight, xadvance, yadvance = (cr.text_extents(label))\n            xbearing, ybearing, kWidth, kHeight, xadvance, yadvance = (cr.text_extents(valueFormatString % (val)))\n        \n            cr.set_source_rgb(LAB_R,LAB_G,LAB_B)\n            cr.move_to(self.X_MARGIN + (self.sWidth - lWidth), self.Y_MARGIN + self.HEADER_SPACE + (self.highest - val) * self.LINE_HEIGHT * (1\/self.delta))\n            cr.show_text(label)\n            \n            cr.set_source_rgb(VAL_R,VAL_G,VAL_B)\n            cr.move_to(self.X_MARGIN + self.sWidth + self.SPACE_WIDTH + (self.startMaxLabelWidth - kWidth), self.Y_MARGIN + self.HEADER_SPACE + (self.highest - val) * self.LINE_HEIGHT * (1\/self.delta))\n            cr.show_text(valueFormatString % (val))\n            \n            cr.stroke()\n        \n        # draw end labels at the correct positions\n        \n        for k in sorted(self.endKeys):\n        \n            val = float(k)\n            label = self.ends[k]\n            xbearing, ybearing, lWidth, lHeight, xadvance, yadvance = (cr.text_extents(label))\n                \n            cr.set_source_rgb(VAL_R,VAL_G,VAL_B)\n            cr.move_to(self.width - self.X_MARGIN - self.SPACE_WIDTH - self.eWidth - self.SPACE_WIDTH - self.endMaxLabelWidth, self.Y_MARGIN + self.HEADER_SPACE + (self.highest - val) * self.LINE_HEIGHT * (1\/self.delta))\n            cr.show_text(valueFormatString % (val))\n        \n            cr.set_source_rgb(LAB_R,LAB_G,LAB_B)\n            cr.move_to(self.width - self.X_MARGIN - self.SPACE_WIDTH - self.eWidth, self.Y_MARGIN + self.HEADER_SPACE + (self.highest - val) * self.LINE_HEIGHT * (1\/self.delta))\n            cr.show_text(label)\n        \n            cr.stroke()\n        \n        # do the actual plotting\n        \n        cr.set_line_width(self.LINE_WIDTH)\n        cr.set_source_rgb(LINE_R, LINE_G, LINE_B)\n        \n        for s1,e1 in self.pairs:\n            cr.move_to(self.X_MARGIN + self.sWidth + self.SPACE_WIDTH + self.startMaxLabelWidth + self.LINE_START_DELTA, self.Y_MARGIN + self.HEADER_SPACE + (self.highest - s1) * self.LINE_HEIGHT * (1\/self.delta) - self.LINE_HEIGHT\/4)\n            cr.line_to(self.width - self.X_MARGIN - self.eWidth - self.SPACE_WIDTH - self.endMaxLabelWidth - self.LINE_START_DELTA, self.Y_MARGIN + self.HEADER_SPACE + (self.highest - e1) * self.LINE_HEIGHT * (1\/self.delta) - self.LINE_HEIGHT\/4)\n            cr.stroke()\n        \n        cr.restore()\n        cr.show_page()\n        \n        if (config['format'] == \"png\"):\n            surface.write_to_png(filename)\n        \n        surface.finish()    \n    \n    def __init__(self, config):\n    \n        # since some methods need these, make them local to the class\n    \n        self.LABEL_FONT_FAMILY = config[\"label_font_family\"]\n        self.LABEL_FONT_SIZE = float(config[\"label_font_size\"])\n        \n        if \"header_font_family\" in config:\n            self.HEADER_FONT_FAMILY = config[\"header_font_family\"]\n            self.HEADER_FONT_SIZE = float(config[\"header_font_size\"])\n        else:\n            self.HEADER_FONT_FAMILY = None\n            self.HEADER_FONT_SIZE = None\n            \n        self.X_MARGIN = float(config[\"x_margin\"])\n        self.Y_MARGIN = float(config[\"y_margin\"])\n        self.LINE_WIDTH = float(config[\"line_width\"])\n\n        if \"slope_length\" in config:\n            self.SLOPE_LENGTH = float(config[\"slope_length\"])\n        else:\n            self.SLOPE_LENGTH = 300\n\n        self.SPACE_WIDTH = self.LABEL_FONT_SIZE \/ 2.0\n        self.LINE_HEIGHT = self.LABEL_FONT_SIZE + (self.LABEL_FONT_SIZE \/ 2.0)\n        self.LINE_START_DELTA = 1.5*self.SPACE_WIDTH\n        \n        OUTPUT_FILE = config[\"output\"] + \".\" + config[\"format\"]\n        \n        # process the values & make the slopegraph\n        \n        self.readCSV(config[\"input\"])\n        self.sortKeys()\n        self.findExtremes()\n        self.calculateExtents(OUTPUT_FILE, config[\"format\"], config[\"value_format_string\"])\n        self.makeSlopegraph(OUTPUT_FILE, config)\n        \n\ndef main():\n\n    parser = argparse.ArgumentParser(description=\"Creates a slopegraph from a CSV source\")\n    parser.add_argument(\"--config\",required=True,\n                    help=\"config file name to use for  slopegraph creation\",)\n    args = parser.parse_args()\n\n    if args.config:\n    \n        json_data = open(args.config)\n        config = json.load(json_data)\n        json_data.close()\n        \n        Slopegraph(config)\n\n    return(0)\n    \nif __name__ == \"__main__\":\n    main()<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>The best way to explain this release will be to walk you through an updated configuration file: { &#8220;label_font_family&#8221; : &#8220;Palatino&#8221;, &#8220;label_font_size&#8221; : &#8220;9&#8221;, &#8220;header_font_family&#8221; : &#8220;Palatino&#8221;, &#8220;header_font_size&#8221; : &#8220;10&#8221;, &#8220;x_margin&#8221; : &#8220;20&#8221;, &#8220;y_margin&#8221; : &#8220;30&#8221;, &#8220;line_width&#8221; : &#8220;0.5&#8221;, &#8220;slope_length&#8221; : &#8220;150&#8221;, &#8220;labels&#8221; : [ &#8220;1970&#8221;, &#8220;1979&#8221; ], &#8220;header_color&#8221; : &#8220;000000&#8221;, &#8220;background_color&#8221; : &#8220;FFFFFF&#8221;, &#8220;label_color&#8221; [&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":true,"_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,7,640],"tags":[751,657,658],"class_list":["post-1174","post","type-post","status-publish","format-standard","hentry","category-charts-graphs","category-development","category-programming","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 More Output Tweaks - 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\/05\/slopegraphs-in-python-more-output-tweaks\/\" \/>\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 More Output Tweaks - rud.is\" \/>\n<meta property=\"og:description\" content=\"The best way to explain this release will be to walk you through an updated configuration file: { &quot;label_font_family&quot; : &quot;Palatino&quot;, &quot;label_font_size&quot; : &quot;9&quot;, &quot;header_font_family&quot; : &quot;Palatino&quot;, &quot;header_font_size&quot; : &quot;10&quot;, &quot;x_margin&quot; : &quot;20&quot;, &quot;y_margin&quot; : &quot;30&quot;, &quot;line_width&quot; : &quot;0.5&quot;, &quot;slope_length&quot; : &quot;150&quot;, &quot;labels&quot; : [ &quot;1970&quot;, &quot;1979&quot; ], &quot;header_color&quot; : &quot;000000&quot;, &quot;background_color&quot; : &quot;FFFFFF&quot;, &quot;label_color&quot; [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/\" \/>\n<meta property=\"og:site_name\" content=\"rud.is\" \/>\n<meta property=\"article:published_time\" content=\"2012-06-05T17:12:53+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2018-03-10T12:53:38+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=\"8 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\/05\/slopegraphs-in-python-more-output-tweaks\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/\"},\"author\":{\"name\":\"hrbrmstr\",\"@id\":\"https:\/\/rud.is\/b\/#\/schema\/person\/d7cb7487ab0527447f7fda5c423ff886\"},\"headline\":\"Slopegraphs in Python \u2013 More Output Tweaks\",\"datePublished\":\"2012-06-05T17:12:53+00:00\",\"dateModified\":\"2018-03-10T12:53:38+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/\"},\"wordCount\":255,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/rud.is\/b\/#\/schema\/person\/d7cb7487ab0527447f7fda5c423ff886\"},\"keywords\":[\"slopegraph\",\"table-chart\",\"tufte\"],\"articleSection\":[\"Charts &amp; Graphs\",\"Development\",\"Programming\",\"Python\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/\",\"url\":\"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/\",\"name\":\"Slopegraphs in Python \u2013 More Output Tweaks - rud.is\",\"isPartOf\":{\"@id\":\"https:\/\/rud.is\/b\/#website\"},\"datePublished\":\"2012-06-05T17:12:53+00:00\",\"dateModified\":\"2018-03-10T12:53:38+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/rud.is\/b\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Slopegraphs in Python \u2013 More Output Tweaks\"}]},{\"@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 More Output Tweaks - 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\/05\/slopegraphs-in-python-more-output-tweaks\/","og_locale":"en_US","og_type":"article","og_title":"Slopegraphs in Python \u2013 More Output Tweaks - rud.is","og_description":"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\" : \"000000\", \"background_color\" : \"FFFFFF\", \"label_color\" [&hellip;]","og_url":"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/","og_site_name":"rud.is","article_published_time":"2012-06-05T17:12:53+00:00","article_modified_time":"2018-03-10T12:53:38+00:00","author":"hrbrmstr","twitter_card":"summary_large_image","twitter_misc":{"Written by":"hrbrmstr","Est. reading time":"8 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/#article","isPartOf":{"@id":"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/"},"author":{"name":"hrbrmstr","@id":"https:\/\/rud.is\/b\/#\/schema\/person\/d7cb7487ab0527447f7fda5c423ff886"},"headline":"Slopegraphs in Python \u2013 More Output Tweaks","datePublished":"2012-06-05T17:12:53+00:00","dateModified":"2018-03-10T12:53:38+00:00","mainEntityOfPage":{"@id":"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/"},"wordCount":255,"commentCount":0,"publisher":{"@id":"https:\/\/rud.is\/b\/#\/schema\/person\/d7cb7487ab0527447f7fda5c423ff886"},"keywords":["slopegraph","table-chart","tufte"],"articleSection":["Charts &amp; Graphs","Development","Programming","Python"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/","url":"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/","name":"Slopegraphs in Python \u2013 More Output Tweaks - rud.is","isPartOf":{"@id":"https:\/\/rud.is\/b\/#website"},"datePublished":"2012-06-05T17:12:53+00:00","dateModified":"2018-03-10T12:53:38+00:00","breadcrumb":{"@id":"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-more-output-tweaks\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/rud.is\/b\/"},{"@type":"ListItem","position":2,"name":"Slopegraphs in Python \u2013 More Output Tweaks"}]},{"@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-iW","jetpack_likes_enabled":true,"jetpack-related-posts":[{"id":1229,"url":"https:\/\/rud.is\/b\/2012\/06\/08\/slopegraphs-in-python-experimental-raphael-support\/","url_meta":{"origin":1174,"position":0},"title":"Slopegraphs in Python \u2013 Experimental Rapha\u00ebl Support","author":"hrbrmstr","date":"2012-06-08","format":false,"excerpt":"In preparation for the upcoming 1.0 release and with the hopes of laying a foundation for more interactive slopegraphs, I threw together some rudimentary output support over lunch today for Rapha\u00ebl, which means that all you have to do is generate a new slopegraph with the \"js\" output type and\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":1249,"url":"https:\/\/rud.is\/b\/2012\/06\/11\/slopegraphs-in-python-formatting-tweaks\/","url_meta":{"origin":1174,"position":1},"title":"Slopegraphs in Python \u2013 Formatting Tweaks","author":"hrbrmstr","date":"2012-06-11","format":false,"excerpt":"There were enough eye-catching glitches in the experimental javascript support and the ugly large-number display in the spam example post that I felt compelled to make a couple formatting tweaks in the code. I also didn't have time to do \"real\" work on the codebase this weekend. So, along with\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":1261,"url":"https:\/\/rud.is\/b\/2012\/06\/12\/slopegraphs-in-python-gratuitous-raphael-animations\/","url_meta":{"origin":1174,"position":2},"title":"Slopegraphs in Python \u2013 Gratuitous Rapha\u00ebl Animations","author":"hrbrmstr","date":"2012-06-12","format":false,"excerpt":"UPDATE: It seems my use of <script async> optimization for Rapha\u00ebl busted the inline slopegraph generation. Will work on tweaking the example posts to wait for Rapha\u00ebl to load when I get some time. So, I had to alter this to start after a user interaction. It loaded fine as\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":1155,"url":"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-the-quintessential-gdp-example-revisited\/","url_meta":{"origin":1174,"position":3},"title":"Slopegraphs \u2013\u00a0The Quintessential Tufte GDP Example Revisited","author":"hrbrmstr","date":"2012-06-05","format":false,"excerpt":"If you're even remotely familiar with slopegraphs, then you'll recognize Tufte's classic 1970-1979 GDP chart example (click for larger version): What you may not notice initially, however, is that Tufte \u2014 well \u2014\u00a0cheated. Yes, I said it. Cheated. I can show it by zooming into the \"Belgium\/Canada\/Finland\" grouping in the\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":1115,"url":"https:\/\/rud.is\/b\/2012\/05\/29\/slopegraphs-in-python-colors-fonts-alignment\/","url_meta":{"origin":1174,"position":4},"title":"Slopegraphs In Python \u2013\u00a0colors, fonts &#038; alignment","author":"hrbrmstr","date":"2012-05-29","format":false,"excerpt":"On the heels of last evening's release of Slopegraphs in Python post comes some minor tweaks: Complete alignment control of labels & and values Colors (for background, lines, labels & values) \u2014 I picked a random pattern from Adobe's Kuler A font change (to prove width calculations work) \u2026and a\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":1181,"url":"https:\/\/rud.is\/b\/2012\/06\/05\/slopegraphs-in-python-slope-colors\/","url_meta":{"origin":1174,"position":5},"title":"Slopegraphs in Python \u2013 Slope Colors","author":"hrbrmstr","date":"2012-06-05","format":false,"excerpt":"As the codebase gets closer to the 1.0 stretch we now have the addition of slope colors for when values go up\/down or remain constant between points. The code still only handles two columns of data, but the intent is for each segment to also be colored appropriately (up\/down\/same) in\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":[]}],"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/posts\/1174","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=1174"}],"version-history":[{"count":0,"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/posts\/1174\/revisions"}],"wp:attachment":[{"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/media?parent=1174"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/categories?post=1174"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rud.is\/b\/wp-json\/wp\/v2\/tags?post=1174"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}