{"id":303,"date":"2022-08-03T19:51:22","date_gmt":"2022-08-03T19:51:22","guid":{"rendered":"https:\/\/mechied.com\/?p=303"},"modified":"2022-08-03T19:52:46","modified_gmt":"2022-08-03T19:52:46","slug":"week-5-creating-an-interactive-project-measurement-dashboard","status":"publish","type":"post","link":"https:\/\/mechied.com\/index.php\/2022\/08\/03\/week-5-creating-an-interactive-project-measurement-dashboard\/","title":{"rendered":"Week 5: Creating an Interactive Project Measurement Dashboard"},"content":{"rendered":"\n<p>The main project I&#8217;m working on at The Recurse Center is an interactive project measurement dashboard. This week I&#8217;ll talk about the current state of the design and construction. I&#8217;m actively working on rearranging the user interface so a second post some other week will be needed to cover that.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Motivation<\/h2>\n\n\n\n<p>My wife, Justine, uses a series of interconnected spreadsheets to model the sales forecast of book publishing projects she&#8217;s managing.  The spreadsheets consider things like sales projections, revenue splits, printing costs, and projected returns to try and come up with a month-by-month view of the business&#8217;s cash flow. The spreadsheets are fairly clever in their implementation, but still have limitations like having to manually re-link fields if a publishing date is changed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Approach<\/h2>\n\n\n\n<p>I reviewed the spreadsheets and determined the core building blocks are Measurements and Visuals.  <\/p>\n\n\n\n<p>Measurements are quantities that describe the project. I noticed a few kinds of measurements:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Fixed Values: <em> 50\/50% revenue split <\/em>(just making these numbers up!)<\/li><li>Fixed Values at Dates: <em>Marketing $10,000 on July 1<\/em><\/li><li>Reoccurring Fixed Values: <em>$1000 Software Fees monthly from July 1 to September 1<\/em><\/li><li>Distributed Values: <em>10,000 Books sold between September 1 and December 1 with strong initial sales followed by a cooldown<\/em><\/li><li>Related Values: <em>Printing cost is $5 times the number of books sold<\/em><\/li><li>Related Values offset in time: <em>Book returns are 10% of books sold with a 1 month lag<\/em><\/li><\/ul>\n\n\n\n<p>Visuals are just the results of measurements and optionally may include some aggregation<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Graphs<\/li><li>Charts<\/li><li>Charts of data totaled by month<\/li><li>Pie Charts<\/li><\/ul>\n\n\n\n<p>I gathered my thoughts and observations into this <a href=\"https:\/\/miro.com\/app\/board\/uXjVOlCbSYo=\/?share_link_id=900069691902\" data-type=\"URL\" data-id=\"https:\/\/miro.com\/app\/board\/uXjVOlCbSYo=\/?share_link_id=900069691902\">Miro Board<\/a> prior to starting development including some thoughts about GUI wireframes and SQL models. I think it&#8217;s always interesting to ideas evolve and looking back at this presentation, I only kept about 50% of the original ideas.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Architecture<\/h2>\n\n\n\n<p>I&#8217;m interested in improving my skills with Python and Javascript so I constructed programs using a Python backend using the Django and Django Rest Framework and Javascript using React.  <\/p>\n\n\n\n<p>The user creates and views measurements in one interface and visualizes the results in a second interface. The second interface stores the state of the users visualizations so that they can easily be recalled.  A REST API is used to communicate with the database. When the user creates new measurements, a Django Signal is triggered that causes the results table to be updated.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"2378\" height=\"1681\" src=\"https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Update-8_3_2022-1.jpg\" alt=\"\" class=\"wp-image-309\" srcset=\"https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Update-8_3_2022-1.jpg 2378w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Update-8_3_2022-1-300x212.jpg 300w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Update-8_3_2022-1-500x353.jpg 500w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Update-8_3_2022-1-768x543.jpg 768w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Update-8_3_2022-1-1536x1086.jpg 1536w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Update-8_3_2022-1-2048x1448.jpg 2048w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Update-8_3_2022-1-400x283.jpg 400w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Update-8_3_2022-1-600x424.jpg 600w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Update-8_3_2022-1-800x566.jpg 800w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Update-8_3_2022-1-1920x1357.jpg 1920w\" sizes=\"auto, (max-width: 2378px) 100vw, 2378px\" \/><\/figure>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\">Handling of Related Measurements<\/h3>\n\n\n\n<p>Creating a view of database state in React is not particularly novel. However allowing a user to create measurements that depend on other measurements required more cleverness and this section describes a little bit about how they are handled. <\/p>\n\n\n\n<p>The first step was defining how the frontend would communicate these measurements. This could have been a dedicated SQL database structure but to simplify implementation I decided to use a string syntax to define related measurements. Here is an example of the syntax:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\"><p>{p33m97l+20}[Test Project:Book Cost]*.25+{p33m104l+20}[Other Project:Marketing Cost]<\/p><\/blockquote>\n\n\n\n<p>The curly braces are what&#8217;s parsed by the database but the square brackets contain more human-friendly text descriptions of what project and measurement are being specified. Inside the curly braces there is a project tag, measurement tag, and time offset code. The project and measurement tags are actually the keys to the project and measurement in the database. The time offset code allows the result to &#8220;lag&#8221; or &#8220;lead&#8221; the measurement. Now since writing this by hand would be a pretty big ask of the users, they can actually select these values in a modal and insert them into the measurement. Users can specify addition, subtraction, multiplication, division, exponents, and parentheses (but more to come on this later).<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"500\" height=\"335\" src=\"https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/image-500x335.png\" alt=\"\" class=\"wp-image-311\" srcset=\"https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/image-500x335.png 500w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/image-300x201.png 300w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/image-400x268.png 400w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/image-600x402.png 600w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/image.png 703w\" sizes=\"auto, (max-width: 500px) 100vw, 500px\" \/><\/figure>\n<\/div>\n\n\n<p>When a user saves a new measurement with this syntax a HTTP POST request is generated to the database. Django first creates a measurement configuration record:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">#views.py\n\nclass MeasureViewSet(ModelViewSet):\n    serializer_class = MeasureSerializer\n\n    def get_queryset(self):\n        return Measure.objects.filter(project=self.kwargs['project_pk'])\n\n    def get_serializer_context(self):\n        return {'project_id': self.kwargs['project_pk'], \"request_data\": self.request.data}<\/code><\/pre>\n\n\n\n<p>A measurement is described by several parameters which are a nested object inside the HTTP POST request. This requires a custom serializer to unpack and generate\/update the nested models.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">#serializers.py\n\nclass MeasureSerializer(serializers.ModelSerializer):\n\n    def create(self, validated_data):\n        parameter_data = validated_data.pop('parameters')\n        project_id = self.context['project_id']\n\n        measure = Measure.objects.create(\n            project_id=project_id, **validated_data)\n\n        for parameter in parameter_data:\n            Parameter.objects.create(measure=measure, **parameter)\n        return measure\n\n    def update(self, instance, validated_data):\n\n        if self.context['request_data'].get('parameters'):\n            parameter_data = self.context['request_data'].pop('parameters')\n            validated_parameter_data = validated_data.pop('parameters')\n            # note that we're using raw data from the request data and not the validated data\n            # when we go to update the nested object because we need the ID field which is stripped\n            # from validated data\n\n            parameter_dict = dict((i.id, i) for i in instance.parameters.all())\n\n            for item_data in parameter_data:\n                if 'id' in item_data:\n                    # if exists id remove from the dict and update\n                    parameter_item = parameter_dict.pop(item_data['id'])\n                    # remove id from validated data as we don't require it.\n                    item_data.pop('id')\n                    # loop through the rest of keys in validated data to assign it to its respective field\n                    for key in item_data.keys():\n                        setattr(parameter_item, key, item_data[key])\n\n                    parameter_item.save()\n                else:\n                    # else create a new object\n                    Parameter.objects.create(measure=instance, **item_data)\n\n        # delete remaining elements because they're not present in my update call\n            if len(parameter_dict) &gt; 0:\n                for item in parameter_dict.values():\n                    item.delete()\n\n        for attr, value in validated_data.items():\n            setattr(instance, attr, value)\n\n        instance.save()\n\n        return instance\n\n    parameters = ParameterSerializer(many=True, read_only=False)\n<\/code><\/pre>\n\n\n\n<p>After the measurement is created, a signal function is called that uses the measurement&#8217;s parameters to perform calculations of the results and updates a table of measurement results. This function is fairly hefty so I won&#8217;t reproduce it here, but I will discuss a few interesting aspects.<\/p>\n\n\n\n<p>The first is what happens to parse the related expression string syntax and calculate the result. A series of regular expressions are used to pull the project, measure, and offset out of the expression string. Then the database queries for those results and puts the actual values into the expression string.  Finally, since I&#8217;m avoiding writing my own parser, the python eval() method is used to act as the calculator (at some risk&#8211; I do pass an <a href=\"https:\/\/www.programiz.com\/python-programming\/methods\/built-in\/eval\" data-type=\"URL\" data-id=\"https:\/\/www.programiz.com\/python-programming\/methods\/built-in\/eval\">empty globals dictionary<\/a> but the user could write some stupid stuff into the string that I&#8217;m not checking for).<\/p>\n\n\n\n<p>Another key aspect is updated related expression measurements that depend on measurements that themselves have dependents.  Suppose we have a measurement tree as follows:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-vp_lg\"><img loading=\"lazy\" decoding=\"async\" width=\"500\" height=\"354\" src=\"https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Frame-6-edited-500x354.jpg\" alt=\"\" class=\"wp-image-314\" srcset=\"https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Frame-6-edited-500x354.jpg 500w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Frame-6-edited-300x212.jpg 300w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Frame-6-edited-768x543.jpg 768w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Frame-6-edited-1536x1086.jpg 1536w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Frame-6-edited-400x283.jpg 400w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Frame-6-edited-600x424.jpg 600w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Frame-6-edited-800x566.jpg 800w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Fulcrum-Concept-Frame-6-edited.jpg 1806w\" sizes=\"auto, (max-width: 500px) 100vw, 500px\" \/><\/figure>\n<\/div>\n\n\n<p>Clearly the order of update will matter or else related measurements will use old data to generate.  Before I wrote the function that handled this in the database I wrote a small python simulation and used the dependency scenario given in the diagram as test data.  In writing it, I used dictionaries and functions like get_measure_by_id to make it clear where later I&#8217;d be using Django queries.  My solution uses a tree data structure and recursion to cause the update to propagate through the tree in the correct order:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">class measure_list():\n    def __init__(self) -&gt; None:                \n        self.data = [measure(1, [2, 3, 4]),\n            measure(2, [5]),\n            measure(3, []),\n            measure(4, []),\n            measure(5, [3, 6, 7, 8]),\n            measure(6, []),\n            measure(7, []),\n            measure(8, [3])]\n\n    def get_measure_by_id(self, id:int):\n        return list(filter(lambda x: x.get_id()==id,self.data))[0]\n\n    def get_dependents_by_id(self,id:int) -&gt; list[int]:\n        return list(filter(lambda x: x.get_id()==id,self.data))[0].get_depends_on()\n\n    def tell_id_to_update(self,id:int):\n        measure = self.get_measure_by_id(id)\n        did_measure_update = measure.update()\n        return did_measure_update\n\nclass measure ():\n    def __init__(self, id: int, depends_on: list[int], ) -&gt; None:\n        self.id = id\n        self.depends_on = depends_on\n        self.is_updated = False\n\n    def set_measure_list(self,measure_list: measure_list) -&gt; None:\n        self.measure_list = measure_list\n\n    def get_id(self) -&gt; int:\n        return self.id\n    def get_depends_on(self) -&gt; list[int]:\n        return self.depends_on\n\n    def update(self):\n        if self.depends_on == []:\n            self.is_updated = True\n            print(str(self.get_id()) + \"has updated\")\n            return True\n        else:\n            results = []\n            for child in self.depends_on:\n                results.append(self.measure_list.tell_id_to_update(child))\n            self.is_updated = True\n            print(str(self.get_id()) + \"has updated\")\n            return True\n\nmy_measure_list = measure_list()\nfor measure in my_measure_list.data:\n    measure.set_measure_list(my_measure_list)\n\nprint(my_measure_list.tell_id_to_update(1))<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"powershell\" class=\"language-powershell\">Output:\n3has updated\n6has updated\n7has updated\n3has updated\n8has updated\n5has updated\n2has updated\n3has updated\n4has updated\n1has updated\nTrue<\/code><\/pre>\n\n\n\n<p>If you look at the output the solution one inefficiency in this approach is that the child &#8220;leaf&#8221; nodes are told to update multiple times.  It&#8217;s not harmful but it is slightly inefficient.  I could store if the child has been updated and skip the update but in the actual Django implementation I didn&#8217;t want to have to manage the &#8220;child has been updated already on this update&#8221; state.  So I&#8217;ve left it for now.  I think it will be workable since the trees are not likely to be overly complex and the database updates are not particularly costly to perform.<\/p>\n\n\n\n<p>Another problem in this implementation is that circular dependency is not handled.  I think that would be easy enough to detect and trigger an error so I plan on fixing it eventually.<\/p>\n\n\n\n<p>The code running on the database looks very similar to this prototype, just with functions like measure_query = Results.objects.filter(measure=measure_id).filter(date=date) used in place of the dictionary functions.<\/p>\n\n\n\n<p>One final big assumption in the code is that since a measurement could have results with null date, a single date, or several dates, a dependent measurement will first look for a child measurement at the same date, then at null date, then it will just assume 0.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n\n\n\n<p>That was a whole lot of discussion of a backend to not really have much to show.  I guess if it looked good it wouldn&#8217;t be the backend?  Regardless I will post a chart to show that this actually works- I&#8217;m able to create, update, and delete measurements that then generate valid results in the database.  In a future week I&#8217;ll share how the user actually makes these charts!<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-vp_xl\"><img loading=\"lazy\" decoding=\"async\" width=\"600\" height=\"337\" src=\"https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-03-at-3.46.51-PM-600x337.png\" alt=\"\" class=\"wp-image-316\" srcset=\"https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-03-at-3.46.51-PM-600x337.png 600w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-03-at-3.46.51-PM-300x169.png 300w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-03-at-3.46.51-PM-500x281.png 500w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-03-at-3.46.51-PM-768x432.png 768w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-03-at-3.46.51-PM-400x225.png 400w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-03-at-3.46.51-PM-800x450.png 800w, https:\/\/mechied.com\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-03-at-3.46.51-PM.png 1058w\" sizes=\"auto, (max-width: 600px) 100vw, 600px\" \/><\/figure>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>The main project I&#8217;m working on at The Recurse Center is an interactive project measurement dashboard. This week I&#8217;ll talk about the current state of the design and construction. I&#8217;m actively working on rearranging the user interface so a second post some other week will be needed to cover that. Motivation My wife, Justine, uses &#8230; <a title=\"Week 5: Creating an Interactive Project Measurement Dashboard\" class=\"read-more\" href=\"https:\/\/mechied.com\/index.php\/2022\/08\/03\/week-5-creating-an-interactive-project-measurement-dashboard\/\" aria-label=\"Read more about Week 5: Creating an Interactive Project Measurement Dashboard\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"_vp_format_video_url":"","_vp_image_focal_point":[],"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[5],"tags":[],"class_list":["post-303","post","type-post","status-publish","format-standard","hentry","category-rc"],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/mechied.com\/index.php\/wp-json\/wp\/v2\/posts\/303","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mechied.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mechied.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mechied.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mechied.com\/index.php\/wp-json\/wp\/v2\/comments?post=303"}],"version-history":[{"count":11,"href":"https:\/\/mechied.com\/index.php\/wp-json\/wp\/v2\/posts\/303\/revisions"}],"predecessor-version":[{"id":319,"href":"https:\/\/mechied.com\/index.php\/wp-json\/wp\/v2\/posts\/303\/revisions\/319"}],"wp:attachment":[{"href":"https:\/\/mechied.com\/index.php\/wp-json\/wp\/v2\/media?parent=303"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mechied.com\/index.php\/wp-json\/wp\/v2\/categories?post=303"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mechied.com\/index.php\/wp-json\/wp\/v2\/tags?post=303"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}