Combo Filtering Using the Chart Legend!

Hello guys and gals!

I’m Shmuel and I’m a new Skuider.

I’ve been learning skuid for about 1 or 2 months now and it is a life changer.
It’s one of those tools that is so easy to use, it becomes a gateway for learning more complex things (kind of like Salesforce itself…).
I become more and more impressed each day. It’s like Skuid has thought of everything possible (not saying there’s not room for growth though).

But it’s not only the tool itself that’s great, the Skuid community is chock full of information and contributions.

Here’s a small contribution to give back! It’s one of those ‘use at your own risk’ kind of things given that I’m completely new to javascript and web development (my experience is mainly in Apex). So, forgive the bad practices when you find them! And also let me know about them :).

The idea behind this snippet came from a user. When presented with a pie chart, he wanted to be able to select multiple parts of the pie and see the data below.
So I put a before render snippet and using the Highcharts API, utilized the onclick event for legendItems.
Now, you can select any combination of a set of points in a series, and the snippet will query another model using the points selected.
My use case was to allow for the user to choose a combination of stages, compare the aggregate data and display data about those stages below.

I didn’t specify in the comments, but I found a few functions unavailable from highcharts which is why you may see some code that looks like it could have been done with a highchart provided method (or I just missed it).

You can put this sample page in a developer org (it will look better if there’s some data in it).

Thanks to Rob Hatch who’s post here led me to the world of HighCharts :slight_smile: https://community.skuid.com/t/show-percentage-lables-in-charts

Enjoy!

<skuidpage unsavedchangeswarning="yes" personalizationmode="server" showsidebar="true" showheader="true">
 <models> <model id="OpportunityAggregate" limit="" query="true" createrowifnonefound="false" adapter="salesforce" type="aggregate" sobject="Opportunity"> <fields> <field id="Id" name="countdistinctId" function="COUNT_DISTINCT"/> </fields> <conditions/> <actions/> <groupby method="simple"> <field id="StageName" name="stageName"/> </groupby> </model> <model id="OpportunityDetail" limit="500" query="false" createrowifnonefound="false" adapter="salesforce" type="" sobject="Opportunity"> <fields> <field id="StageName"/> <field id="Attachments" type="childRelationship" limit="10"> <fields> <field id="Id"/> <field id="Name"/> </fields> </field> <field id="OpportunityContactRoles" type="childRelationship" limit="10"> <conditions/> <fields> <field id="Contact&#46;Name"/> </fields> </field> <field id="OpportunityCompetitors" type="childRelationship" limit="10"> <fields> <field id="CompetitorName"/> </fields> </field> <field id="OpportunityLineItems" type="childRelationship" limit="10"> <fields> <field id="Name"/> </fields> </field> </fields> <conditions logic=""> <condition type="multiple" value="" field="StageName" operator="in" enclosevalueinquotes="true" state="filterableon" inactive="false" name="StageNames"> <values> <value/> <value/> </values> </condition> </conditions> <actions/> </model> </models> <components> <skuidvis__chart model="OpportunityAggregate" maintitle="Opportunties" type="pie" uniqueid="sk-3jSKYn-178" rendersnippet="ChartLegendFilter"> <dataaxes> <axis id="axis1"/> </dataaxes> <categoryaxes> <axis id="categories" categorytype="field"/> </categoryaxes> <serieslist> <series valuefield="countdistinctId" splittype="template" modelId="OpportunityAggregate" splittemplate="{{{stageName}}}" aggfunction="sum"/> </serieslist> <colors/> <legend layout="horizontal" halign="center" valign="bottom"/> <renderconditions logictype="and"/> </skuidvis__chart> <skootable showconditions="true" showsavecancel="false" showerrorsinline="true" searchmethod="server" searchbox="true" showexportbuttons="false" pagesize="50" createrecords="false" model="OpportunityDetail" buttonposition="" mode="readonly" uniqueid="sk-3jSgRb-196"> <fields> <field id="Name" valuehalign="" type=""/> <field id="StageName" valuehalign="" type=""/> <field id="OpportunityContactRoles" type="CHILDREL" limit="100" valuehalign="" allowhtml="true"> <label>Contact Roles</label> <template>&amp;lt;ul style="margin: 0; padding: 0;"&amp;gt; &amp;lt;li&amp;gt;{{Contact&#46;Name}}&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt; &amp;lt;/ul&amp;gt;</template> </field> <field id="OpportunityCompetitors" type="CHILDREL" limit="100" valuehalign="" allowhtml="true"> <label>Competitors</label> <template>&amp;lt;ul style="margin: 0; padding: 0;"&amp;gt; &amp;lt;li&amp;gt;{{CompetitorName}}&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt; &amp;lt;/ul&amp;gt;</template> </field> <field id="OpportunityLineItems" type="CHILDREL" limit="100" valuehalign="" allowhtml="true"> <label>Line Items</label> <template>&amp;lt;ul style="margin: 0; padding: 0;"&amp;gt; &amp;lt;li&amp;gt;{{Name}}&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt; &amp;lt;/ul&amp;gt;</template> </field> <field id="Attachments" type="CHILDREL" limit="100" valuehalign="" allowhtml="true"> <label>Attachments</label> <template>&amp;lt;ul style="margin: 0; padding: 0;"&amp;gt; &amp;lt;li&amp;gt;&amp;lt;a href="/{{{Id}}}" target="_blank"&amp;gt;{{Name}}&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt; &amp;lt;/ul&amp;gt; </template> </field> </fields> <rowactions/> <massactions usefirstitemasdefault="true"/> <views> <view type="standard"/> </views> <searchfields/> <renderconditions logictype="and"/> </skootable> </components> <resources> <labels/> <javascript> <jsitem location="inlinesnippet" name="ChartLegendFilter" cachelocation="false">var chartObj = arguments[0], $ = skuid&#46;$; $&#46;extend(true, chartObj, { chart : { backgroundColor: "#FFFFFF" }, tooltip: { enabled: false }, plotOptions: { pie: { events: { afterAnimate: function() { SetAllPointsToVisible(); } }, allowPointSelect: true, point: { events: { legendItemClick: function() { try { &#47;&#47;feed point to main filter handler filterBySelectedItems(this); } catch (eForError) { console&#46;log(eForError); } } } }, dataLabels: { enabled: true, formatter: function() { return this&#46;point&#46;name + ' ' + this&#46;point&#46;y + ' (' + this&#46;percentage&#46;toFixed(2) + '%)'; } } } } }); &#47;&#47;since the point&#46;visible attribute isn't accessible before the legenditem/point is clicked, &#47;&#47;we need to set all points to visible function SetAllPointsToVisible() { &#47;&#47;get all of the points in your series &#47;&#47;WARNING I ONLY HAD ONE SERIES, YOU MAY HAVE TO DO THIS FOR &#47;&#47;ALL SERIES&#46; var points = chartObj&#46;series[0]&#46;data; try { for (i = 0; i &amp;lt; points&#46;length; i++) { var point = points[i]; point&#46;select; point&#46;visible = true; } } catch (e) { console&#46;log(e); } } &#47;&#47;this is where you update the conditions function updateConditions(conditionList) { oppLegacyMod = skuid&#46;model&#46;getModel('OpportunityDetail'); &#47;&#47;remove all rows if there are any oppLegacyMod&#46;emptyData(); &#47;&#47;get condition var stageConditions = oppLegacyMod&#46;getConditionByName('StageNames'); &#47;&#47;update query condition oppLegacyMod&#46;setCondition(stageConditions, conditionList); &#47;&#47;update model oppLegacyMod&#46;updateData(); } &#47;&#47;each time a legendItem is clicked, this function is called &#47;&#47;to get the currently clicked legenditems as of this click function CreateActivePointsList(currentPoint) { var activePointsList = Array(); &#47;&#47;get all points var points = chartObj&#46;series[0]&#46;data; &#47;&#47;iterate over all points for (i = 0; i &amp;lt; points&#46;length; i++) { &#47;&#47;declare a point for the current iteration var point = points[i]; &#47;&#47;exception for the current point which was just clicked via the UI &#47;&#47;since its 'visible' attribute doesn't change until after the onclick event has finished &#47;&#47;basically the value you want (post click) is the opposite of what the registered attribute value is &#47;&#47;(pre click) if (point&#46;name == currentPoint&#46;name &amp;amp;&amp;amp; currentPoint&#46;visible) { continue; } else if (point&#46;name == currentPoint&#46;name) { activePointsList&#46;push(point&#46;name); continue; } if (point&#46;visible) { activePointsList&#46;push(point&#46;name); } } return activePointsList; } &#47;&#47;need to store active points in sessionStorage &#47;&#47;since each time a legend item is clicked, a new context is started, and variables are not &#47;&#47;saved across contexts function CreateSessionStorageArray() { &#47;&#47;check to see if the storage item was created this session if (sessionStorage&#46;getItem("activePoints") === null) { &#47;&#47;if it hasn't been, create it var activePointsTemp = []; sessionStorage&#46;setItem("activePoints", activePointsTemp&#46;toString()); } } function GetActivePointsGlobal() { &#47;&#47;get and return activePoints return sessionStorage&#46;getItem("activePoints"); } function timedModelUpdate(timeToBlockUIFuncVar, activePointsFuncVar) { &#47;&#47;declare array which will be populated by the sessionStorage variable var activePointsGlobal = []; &#47;&#47;sessionStorage items are stored as strings&#46; &#47;&#47;Since the active points object is an array, it's stored as a comma separated list &#47;&#47; and can be converted back into an array with the string&#46;split() method activePointsGlobal = GetActivePointsGlobal()&#46;split(","); &#47;&#47;make sure that the list of points that this snippet has calculated as active is the most current &#47;&#47;list (the user could have clicked on different points since then) var isSame = JSON&#46;stringify(activePointsFuncVar) === JSON&#46;stringify(activePointsGlobal); &#47;&#47;if the lists are the same it means that this snippet is the last run snippet &#47;&#47; - from the time the initial click was made to the until the amount of time to wait has elapsed&#46; &#47;&#47;Therefore, use these values as they are current! if (isSame) { &#47;&#47;block the UI so the user doesn't click anymore legend items before the query had a chance to be retrieved and displayed skuid&#46;$&#46;blockUI({ message: 'Populating Table With Selected Options', timeout: timeToBlockUIFuncVar }); &#47;&#47;update the conditions with the points calculated from this snippet &#47;&#47;(or whatever context-local variable(s) you want to populated it with) &#47;&#47;the updatedata is called within the update conditions function updateConditions(activePointsFuncVar); &#47;&#47;set active points to an empty string just in case it ever gets referenced elsewhere (not 100% necessary) sessionStorage&#46;setItem("activePoints", ""); } } function filterBySelectedItems(pointClicked) { &#47;&#47;active points as of the time the legendItem was clicked var activePoints = CreateActivePointsList(pointClicked); &#47;&#47;create sessionStorage if one hasn't been created already CreateSessionStorageArray(); &#47;&#47;overwrite sessionStorage with current list since the list we want will always &#47;&#47;be the last clicked list sessionStorage&#46;setItem("activePoints", activePoints&#46;toString()); &#47;&#47;time to wait from last click until you're done waiting for the user to finish inputting &#47;&#47;this gives the user time to choose different options&#46; &#47;&#47;in milliseconds var timeToWait = 2000; &#47;&#47;time to block UI for in milliseconds var timeToBlockUI = 2000; &#47;&#47;launch timeout function &#47;&#47;the point of the logic in the timeout function is to make sure only the last clicked legend item's snippet is acted on &#47;&#47;(i&#46;e it will run the update conditions)&#46; &#47;&#47;if the snippet is not from the last clicked legend item otherwise it will do nothing setTimeout( function() { timedModelUpdate(timeToBlockUI, activePoints); }, timeToWait); }</jsitem> </javascript> <css/> </resources> <styles> <styleitem type="background" bgtype="none"/> </styles> </skuidpage>