Toggle navigation
Home
▼ Details
Products and pricing
Chart gallery
User stories
Text analytics
CDC NAMCS Library
Blog
Tutorials
Contact
Sign in
Post Editor
← All help posts
View post
Save
<div class="article-body-content"><p>Protobi has a wide array of charts readily available to choose from in the chart type dialog. If none of the charts meet your specific needs, you can create your own custom charts using JavaScript and CSS. </p><p>In this article, we review how the TargetSorter works and look at the process of adding a custom chart to your project.</p><h2 id="targetsorter-components-0" data-toc="true">TargetSorter components</h2><p>The TargetSorter is a custom, sortable table that consists of four components. </p><figure class="image"><img src="/uploads/upload/image/5200/direct/1691434622508-image.png" alt="Target Sorter view displaying a data table with Banner group, Banner point, and N columns showing demographic segments (GENDER, AGE, EDUCATION, REGION, ETHNICITY) cross-tabulated by "PARTY ID & IDEOLOGY" with values for Liberal Democrats, Non-Liberal Democrats, Liberal Independent, and Non-Liberal Independent. A Groups panel on the right lists available grouping variables including GENDER, AGE, EDUCATION, REGION, ETHNICITY, PARTY ID, IDEOLOGY, and various targeting options." width="902px"></figure><p><strong>1. Banner group</strong></p><p>The banner groups in the TargetSorter typically include all the project banners you want to cut your data by. The banners make up the rows of the table. You add banner groups by adding them as children of the TargetSorter. </p><p><strong>2. Banner point and sample size</strong></p><p>Every data point within each banner will appear under the banner points section of the TargetSorter. This section also shows the sample size for each banner point. </p><p><strong>3. Target values and </strong><strong>frequencies</strong></p><p>This section represents the crosstab on the TargetSorter. Each value from the crosstab element is represented by a column. The numbers represent the value frequency per banner point. Press on the headers to sort by a target value.</p><p><strong>4. Groups picker</strong></p><p>The groups picker shows every banner group that is included in the TargetSorter. Select the banner groups that you want to hide by pressing on them. Hidden groups will show with the strikethrough text format. To unhide them, press again.</p><h2 id="sort-by-a-target-value-1" data-toc="true">Sort by a target value</h2><p>Press on the target values to sort the frequencies in descending order. This allows you to better understand which banner groups/points are associated with high frequency. </p><figure class="image"><img src="/uploads/upload/image/5200/direct/1691427305028-click_to_sort_targetsorter.gif" width="902px"></figure><h2 id="hide-rows-to-bring-groups-of-interest-to-the-top-2" data-toc="true">Hide rows to bring groups of interest to the top</h2><p>Press on the groups under the picker to hide/unhide banner rows. </p><figure class="image"><img src="/uploads/upload/image/5200/direct/1691427309700-click_to_collapse_stubs_targetsorter.gif" width="902px"></figure><h2 id="set-up-a-custom-chart-3" data-toc="true">Set up a custom chart</h2><p id="setting-up-a-custom-chart-3" data-toc="true">1. <a href="https://help.protobi.com/how-to/create-a-new-element#Create%20an%20empty%20element-2">Create a new element</a> </p><p id="setting-up-a-custom-chart-3" data-toc="true">Press on the + sign to add a new element. Protobi will prompt you to create a new key for the element. </p><figure class="image"><img src="/uploads/upload/image/5200/direct/1691504607254-image.png" alt="Analysis view showing "PARTY ID & IDEOLOGY" data with a bar chart displaying Liberal Democrats (54), Non-Liberal Democrats (25), Liberal Independent/Republicans (8), and Non-Liberal Independent/Republicans (12), N=600. Below is an "Add an element" dialog box with a text field containing "banners_targetsorter" and Cancel/Ok buttons." width="659px"></figure><p id="setting-up-a-custom-chart-3" data-toc="true">2. From the <a href="https://help.protobi.com/editaccess/edit-json#%0AJSON%20editor-0">JSON editor</a>, set the chartType to "BasicElement". </p><p id="setting-up-a-custom-chart-3" data-toc="true">The BasicElement chartType is used for custom charts. It's not an option in the chartType dialog, so we add the chartType manually in the JSON editor.</p><figure class="image"><img src="/uploads/upload/image/5200/direct/1691504742110-image.png" alt="Element properties dialog for "banners_targetsorter" showing JSON configuration with key "banners_targetsorter", roundby set to 0, chartType as "BasicElement", and chartOptions including showBars: true, showSignificance: true, limit: 0, width: 400, margin settings, powerpoint exportAs, showOverall: true, and empty footnote object." width="577px"></figure><p> </p><p id="setting-up-a-custom-chart-3" data-toc="true">3. Edit the HTML content editor by selecting the element and using the shortcut SHIFT+X+C.</p><p id="setting-up-a-custom-chart-3" data-toc="true">You can write HTML, Markdown or <a href="/api-references/how-to-use-embedded-javascript-in-protobi">Embedded JavaScript Templating</a> in the content editor to create a custom chart. </p><p id="setting-up-a-custom-chart-3" data-toc="true">Embedded JavaScript Templating lets us embed JavaScript code in a template language that is used to generate HTML. You can insert dynamic data with Embedded JavaScript Templating, but with HTML you can only insert static text. See examples from <a href="/intermediate-editor/create-custom-charts-and-visuals">Create custom charts</a>. </p><figure class="image"><img src="/uploads/upload/image/5200/direct/1691504904699-image.png" alt="Edit custom content dialog for "banners_targetsorter" displaying JavaScript code that begins with console.log("render banner sorter"), sets up variables like showMargin = true, performs initial setup checking for 'col' and 'children' in the model, and retrieves settings including showMissing, showOverall, showFormat, chartOptions, margin, and showBasis variables." width="571px"></figure><h2 id="add-the-targetsorter-to-your-project-4" data-toc="true">Add the TargetSorter to your project</h2><p><strong>Specify children</strong></p><p>Add child elements under properties.<strong>* </strong>Set the banner groups you want to include in the TargetSorter as children in your custom element. The children can be single elements, or groups. In this example, we use banners which includes many elements (Gender, Age, Education, etc). </p><p><strong>*</strong>Note: Not every custom chart has children. Some might use the field attribute as the source of data like in <a href="https://help.protobi.com/intermediate-editor/create-custom-charts-and-visuals#Square%20graphic%20with%20a%20dynamic%20data%20point-4">this simple example with just one data point</a>. </p><figure class="image"><img src="/uploads/upload/image/5200/direct/1691520414578-image.png" alt="More properties dialog displaying configuration fields for Target Sorter element: key is "banners_targetsorter", displayKey is "Target Sorter", headline and title show "text", footnote shows "text", type dropdown is set to "(default)", field shows "data field", element shows "element key", and children field contains "banners"." width="395px"></figure><p><strong>Add the crosstab</strong></p><p>The crosstab becomes the target values columns. In this TargetSorter we use “PARTY ID & IDEOLOGY”. </p><figure class="image"><img src="/uploads/upload/image/5200/direct/1691671806368-image.png" alt="Target Sorter output displaying three sections: main "Target Sorter by PARTY ID & IDEOLOGY" header, followed by "banners by PARTY ID & IDEOLOGY", then nested demographic crosstabs for GENDER and AGE by "PARTY ID & IDEOLOGY". The GENDER table shows Men (40% Overall, 39% Liberal Democrats, 37% Non-Liberal Democrats, 50% Liberal Independent/Republicans, 41% Non-Liberal Independent/Republicans) and Women (60, 61, 63, 50, 59), N=600, Chi Square=3 (p=0.43). The AGE table displays five age groups with similar column structure and Chi Square=9 (p=0.67)." width="648px"></figure><p><strong>Insert content and style</strong></p><p>In the content editor (SHIFT+X+C) add the following Embedded JavaScript Templating code. See the comments within the code that describe what each block does. You can edit this example to fit your needs, or reach out to <a href="mailto:support@protobi.com">support@protobi.com</a> and we will work with you to create a custom chart for your project. </p><div class="helpjuice-accordion" data-controller="editor--toggle-element"> <h2 class="helpjuice-accordion-title">Content</h2> <div class="helpjuice-accordion-body active" data-editor--toggle-element-target="body"><pre><code class="language-plain"><% console.log("render banner sorter") var self = this showMargin = true // initial setup if (!model.get('col')) model.set('col', ['COUNTRY']) if (!model.get('children')) model.set('children', ['banners']) // get settings we'll refer to several times var showMissing = true var showOverall = getShowOverall() var showFormat = getShowFormat() var chartOptions = getChartOptions() var margin = chartOptions.margin; let showBasis = model.get('showBasis') !== false var byDims = getCrosstabDimensions() var byDim = byDims[0] // right now only use first banner var width = chartOptions.width-20 || 800 var innerWidth = width - (margin.r||200) var kids = getVisibleChildren(model) // exclude if hidden:true // if kids is one group, e.g. "banners", expand the group if (kids.length==1 && isGroup(kids[0])) kids = getVisibleChildren(kids[0]) // get one crosstab per kid this.data = kids.reduce(function(acc,kid,arr) { acc[kid.get('key')] = tabular.getCrosstab(byDim, kid, model.get('filter'), {showOverall: false}) return acc }, {}) // get marginal distribution of the banner variable var marg = tabular.getMarginal(byDim, 'current', model.get('filter')) var byDimFormat = byDim.get('format') var byKeys = marg.getKeys().filter(k=>!isNA(k)) var titleColWidth = margin.l / 2 let numDataColumns = byKeys.length + 1 var dataColWidth if (showBasis) { dataColWidth = (innerWidth - margin.l -25 - 40) / byKeys.length } else { dataColWidth = (innerWidth - margin.l-25) / byKeys.length } // assemble a data structure we can sort and render var i=0 var entries = _.flatten( kids .filter(kid=>!model.getDeep(['excluded', kid.get('key')])) .map(function(kid, kidIdx) { var xtab = self.data[kid.get('key')] var xkeys = xtab.columnKeys.map(x=>x[0]) return xkeys.map(function(xKey, xKeyIdx) { return { row: i, dim: kid, rowKey: xKey, xtab: xtab, basis: xtab.getBasis(xKey, showMissing), bannerLabel: kid.getDisplayKey(), pointLabel: toDisplayString(xKey, kid.get('format'), kid), vals: byKeys.reduce(function(acc, byKey, arr) { acc[byKey] = xtab.getRowPercent(xKey, byKey, showMissing) return acc }, {}) } }) }) ) var orderby = model.get('orderby') if (orderby) { entries = _.sortBy(entries, function(entry) { return _.get(entry, orderby) }).reverse() } listenTo(viewModel, 'change:showFormat', renderBody) %> <div class="banner-sorter"> <table class="pure-table results" style="width: <%=innerWidth%>px; max-width: <%=innerWidth%>px; min-width: <%=innerWidth%>px; display: inline-block"> <thead> <tr> <th class="label" style="width: <%=titleColWidth%>px; max-width: <%=titleColWidth%>px; min-width: <%=titleColWidth%>px; " data-orderby="row">Banner group</th> <th class="value" style="width: <%=titleColWidth%>px; max-width: <%=titleColWidth%>px; min-width: <%=titleColWidth%>px; " data-orderby="row">Banner point</th> <% if (showBasis) {%><th class="n">N</th><%}%> <% _.each(byKeys, function(key, idx, arr) { %> <th class="qty <%=model.get('orderby')==key ? 'sort desc' : ''%>" style="width: <%=dataColWidth%>px; max-width: <%=dataColWidth%>px; min-width: <%=dataColWidth%>px" data-key="<%=key%>" data-orderby="vals.<%=key%>"><div title=<%=JSON.stringify(showFormat ? toDisplayString(key, byDim.get('format'), byDim) : key)%>><%=showFormat ? toDisplayString(key, byDim.get('format'), byDim) : key%></div></th> <% }) %> </tr> <tr> <th></th> <th></th> <% if (showBasis) {%><th class=""></th><%}%> <% _.each(byKeys, function(key, idx, arr) { %> <th class="qty" data-orderby="vals.<%=key%>" style="width: <%=dataColWidth%>px; max-width: <%=dataColWidth%>px; min-width: <%=dataColWidth%>px" data-key="<%=key%>" data-orderby="vals.<%=key%>"><%=model.getDeep('orderby')==("vals."+key) ? '▼': ''%></th> <% }) %> </tr> </thead> <tbody> <% _.each(entries, function(entry, entryIdx) { %> <tr> <td class="label" style="width: <%=titleColWidth%>px; max-width: <%=titleColWidth%>px; min-width: <%=titleColWidth%>px; "><%=showFormat ? entry.bannerLabel: entry.dim.get('key') %></td> <td class="value" style="width: <%=titleColWidth%>px; max-width: <%=titleColWidth%>px; min-width: <%=titleColWidth%>px;" title="<%=entry.pointLabel%>"> <%=showFormat ? entry.pointLabel : entry.rowKey%></td> <% if (showBasis) {%><td class="n"><%=fmtFreq(entry.basis)%></td><%}%> <% _.each(byKeys, function(byKey, byIdx, arr) { %> <% var qty = entry.vals[byKey] %> <td class="qty" style="width: <%=dataColWidth%>px; max-width: <%=dataColWidth%>px; min-width: <%=dataColWidth%>px"><%=fmtPercent(qty)%></td> <% }) %> </tr> <% }) %> </tbody> </table> <table class='pure-table selector' style=" display: inline-block; width=<%=(margin.r)%>px; min-width=<%=(margin.r)%>px; max-width: <%=(margin.r)%>px; vertical-align: top"> <thead> <tr> <th>Groups</th> </tr> </thead> <tbody> <% _.each(kids, function(kid, kidIdx) { %> <tr> <td class="<%=model.getDeep(['excluded', kid.get('key')]) ? 'excluded' :''%> pill" draggable="true" data-key="<%=kid.get('key')%>"><%=kid.getDisplayKey()%></td> </tr> <% }) %> </tbody> </table> <style contenteditable style="width: <%=innerWidth%>px; max-width: <%=innerWidth%>px; min-width: <%=innerWidth%>px; display: none"><%= model.getDeep('chartOptions.style')%></style> <% $el.on('blur', 'style', function(evt) { var style = $(evt.currentTarget).text() model.setDeep('chartOptions.style', style) }) function setListeners() { if ($el.visible(true)) { $el.off('click', 'table.selector th') $el.on('click', 'th', function(evt) { var $tgt = $(evt.currentTarget) var key = $tgt.attr('data-orderby') model.set('orderby', key) }) $el.off('click', 'table.selector td') $el.on('click', 'table.selector td', function(evt) { var $tgt = $(evt.currentTarget) var key = $tgt.attr('data-key') var excluded = model.getDeep(['excluded', key]) console.log("Click th.qty",key,!excluded) model.setDeep(['excluded', key], !excluded) renderBody() }) } } on('render', setListeners) viewModel.on('scroll', setListeners) %> </div> </code></pre></div> <div class="helpjuice-accordion-toggle"> </div> <div class="helpjuice-accordion-delete"> </div> </div><p id="add-style-sheets-within-chartoptions-6" data-toc="true">The following CSS belongs in the style attribute within chartOptions. You can copy + paste the entire block into the JSON of your custom element. CSS describes how the HTML elements are to be displayed. You can use CSS to change the font, color, size, or spacing of the chart content. </p><div class="helpjuice-accordion" data-controller="editor--toggle-element"> <h2 class="helpjuice-accordion-title">Style</h2> <div class="helpjuice-accordion-body active" data-editor--toggle-element-target="body"><pre><code class="language-plain">"chartOptions": { "style": " .banner-sorter style { display: block; border: 1px dashed grey; white-space: pre; border: 2px solid grey; margin: 2px; border-radius: 5px; border-collapse: collapse; box-sizing: border-box; } .banner-sorter table { border: 2px solid grey !important; border-radius: 5px; table-layout: fixed; margin: 2px; } .banner-sorter table thead { display: block; ÷background: #ccc; } .banner-sorter table tbody { display: block; overflow-y: auto; height: 1600px; } .banner-sorter table tr:hover { background: #eee } .banner-sorter table th { padding: 5px 10px; box-sizing: border-box !important; background: #ccc; vertical-align: top; } .banner-sorter table td { padding: 2px 10px !important; box-sizing: border-box !important; } .banner-sorter table.selector td:hover { background-color: #DEF; } .banner-sorter table.selector td.excluded { background-color: #EEE; text-decoration: line-through } .banner-sorter table.selector td.excluded:hover { background-color: #CCC; } table.groups td.excluded {background-color: #FC0} "}</code></pre></div> <div class="helpjuice-accordion-toggle"> </div> <div class="helpjuice-accordion-delete"> </div> </div><h2 id="-5" data-toc="true"> </h2><h2 id="learn-more-6" data-toc="true">Learn more</h2><p>See the links below for more information on HTML, Markdown and Lodash Javascript which is a more concise and maintainable form of JavaScript.</p><p>Helpful articles:</p><ul> <li><a href="https://www.w3schools.com/tags/ref_byfunc.asp">HTML tags by category</a></li> <li><a href="https://www.markdownguide.org/basic-syntax/">Basic markdown vs HTML syntax</a></li> <li><a href="https://simplemde.com/markdown-guide">Markdown guide</a></li> <li> <a href="https://lodash.com/docs/">Lodash documentation</a> </li> <li><a href="https://www.geeksforgeeks.org/lodash/">Learn Lodash</a></li> <li><a href="https://javascript.plainenglish.io/11-super-handy-lodash-methods-to-simplify-things-in-javascript-8c45cd346616">11 Super Handy Lodash Methods to Simplify Things in JavaScript</a></li> <li><a href="https://www.w3schools.com/css/">CSS tutorial</a></li> </ul></div>
Publishing
Date
Status
Published
Draft
Slug
edit
Content
Thumbnail
Categories
Manage
New to Protobi?
Charts
Making Changes
Intermediate topics for editors
Frequently Asked Questions
SERMO Topics
Tutorial Pages
Internal Docs
Data Processing
Videos
Obsolete
GSG Admin
SERMO Admin
GSG Topics
How to...
Basics for viewers
Basics for editors
For project admins
Advanced topics
Assessments
Articles (in-progress)
New and updated articles
Process data in Protobi
superseded
Text open-end questions
Troubleshooting
Tracking studies
Organizing the view
API References
Tools
AI Database
Checking AI...
Convert to MD
Danger zone
Delete