├── .gitignore ├── LICENSE.md ├── README.md ├── _config.yml ├── build └── webcodebook.js ├── css └── webcodebook.css ├── package-lock.json ├── package.json ├── rollup.config.js ├── settings-schema.json ├── src ├── charts.js ├── charts │ ├── createDotPlot.js │ ├── createHistogramBoxPlot.js │ ├── createHistogramBoxPlotControls.js │ ├── createHorizontalBars.js │ ├── createHorizontalBarsControls.js │ ├── createSpark.js │ ├── createVerticalBars.js │ ├── createVerticalBarsControls.js │ ├── dotPlot │ │ ├── drawOverallMark.js │ │ ├── modifyOverallLegendMark.js │ │ ├── moveYaxis.js │ │ └── onResize.js │ ├── histogramBoxPlot │ │ ├── addBoxPlot.js │ │ ├── addHighlightMarks.js │ │ ├── addModals.js │ │ ├── defaultSettings.js │ │ ├── defineHistogram.js │ │ ├── makeTooltip.js │ │ ├── moveXaxis.js │ │ ├── moveYaxis.js │ │ ├── onInit.js │ │ └── onResize.js │ ├── horizontalBars │ │ ├── drawDifferences.js │ │ ├── drawOverallMark.js │ │ ├── moveYaxis.js │ │ ├── onInit.js │ │ └── onResize.js │ ├── spark │ │ └── makeHist.js │ ├── util │ │ └── highlightData.js │ └── verticalBars │ │ ├── axisSort.js │ │ ├── makeTooltip.js │ │ ├── moveYaxis.js │ │ ├── onInit.js │ │ └── onResize.js ├── codebook │ ├── chartMaker.js │ ├── chartMaker │ │ ├── chartMakerSettings.js │ │ ├── columnSelect │ │ │ ├── init.js │ │ │ └── initAxisSelect.js │ │ ├── draw.js │ │ ├── init.js │ │ └── makeSettings.js │ ├── controls.js │ ├── controls │ │ ├── controlToggle.js │ │ ├── controlToggle │ │ │ ├── init.js │ │ │ └── set.js │ │ ├── filters.js │ │ ├── filters │ │ │ ├── init.js │ │ │ └── update.js │ │ ├── groups.js │ │ ├── groups │ │ │ ├── init.js │ │ │ └── update.js │ │ └── init.js │ ├── data.js │ ├── data │ │ ├── makeFiltered.js │ │ ├── makeSummary.js │ │ └── summarize │ │ │ ├── categorical.js │ │ │ ├── continuous.js │ │ │ ├── determineType.js │ │ │ └── index.js │ ├── dataListing.js │ ├── dataListing │ │ ├── init.js │ │ └── onDraw.js │ ├── defaultSettings.js │ ├── init.js │ ├── instructions.js │ ├── instructions │ │ ├── chartToggle │ │ │ └── init.js │ │ ├── init.js │ │ └── update.js │ ├── layout.js │ ├── nav.js │ ├── nav │ │ ├── availableTabs.js │ │ └── init.js │ ├── settings.js │ ├── settings │ │ ├── init.js │ │ ├── layout.js │ │ ├── updateSettings.js │ │ └── updateSettings │ │ │ └── reset.js │ ├── summaryTable.js │ ├── summaryTable │ │ ├── draw.js │ │ ├── renderRow.js │ │ └── renderRow │ │ │ ├── details │ │ │ ├── clearDetails.js │ │ │ ├── detailList.js │ │ │ ├── renderMeta.js │ │ │ ├── renderStats.js │ │ │ └── renderValues.js │ │ │ ├── makeChart.js │ │ │ ├── makeDetails.js │ │ │ ├── makeMeta.js │ │ │ └── makeTitle.js │ ├── title.js │ ├── title │ │ ├── highlight.js │ │ ├── highlight │ │ │ └── init.js │ │ ├── init.js │ │ └── updateCountSummary.js │ ├── util.js │ └── util │ │ ├── getBinCounts.js │ │ ├── indicateLoading.js │ │ ├── makeAutomaticFilters.js │ │ ├── makeAutomaticGroups.js │ │ └── setDefaults.js ├── createCodebook.js ├── createExplorer.js ├── explorer │ ├── addFile.js │ ├── defaultSettings.js │ ├── fileListing.js │ ├── fileListing │ │ ├── init.js │ │ └── onDraw.js │ ├── init.js │ ├── initFileLoad.js │ ├── layout.js │ ├── makeCodebook.js │ └── setDefaults.js ├── index.js ├── polyfills │ ├── array-find.js │ ├── array-findIndex.js │ ├── index.js │ └── object-assign.js └── util │ └── clone.js └── test-page ├── default ├── index.css ├── index.html └── index.js └── explorer ├── explorer.js ├── index.css └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | *.sublime* 4 | 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 [Rho Inc.](http://www.rhoworld.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Codebook 2 | 3 | ![alt tag](https://user-images.githubusercontent.com/31038805/33682586-fb48c2cc-da95-11e7-87d9-79982b1aa8ed.gif) 4 | 5 | ## Overview 6 | 7 | The web codebook is a JavaScript library that provides a concise summary of every variable in a dataset. The codebook includes interactive features such as real-time filters and requires minimal user configuration. 8 | [Click here for a live demo](https://rhoinc.github.io/web-codebook/test-page/default/). When the page loads, the user sees a "codebook" providing a graphical data summary for each data column. 9 | 10 | 11 | ![alt tag](https://user-images.githubusercontent.com/31038805/33683185-0f6d9c44-da98-11e7-829d-24f41e77ffc2.gif) 12 | 13 | The library also provides an `explorer` function ([demo](https://rhoinc.github.io/web-codebook/test-page/explorer/)) that lets users explorer codebooks for multiple files from the same webpage. Finally, the associated [datadigest R package](https://github.com/RhoInc/datadigest) wraps the web-codebook functionality in easy-to-use R functions. 14 | 15 | ## Background 16 | 17 | The web-codebook is inspired Frank Harrell's excellent `summarize` method from the [Hmisc R package](https://cran.r-project.org/web/packages/Hmisc/Hmisc.pdf). `summarize` creates concise data summaries with minimal user configuration. Further, [Agustin Calatroni](http://graphics.rhoworld.com/pubs/SCT2007_Calatroni.pdf) and [Shane Rosanbalm](https://github.com/RhoInc/sas-codebook) have created SAS data summary methods at Rho in recent years. Our goal here is to create a web-based data summary that uses the same general principles (concise data display, minimal configuration), but with added interactivity (filters, paneled displays, data listings) that is not possible in the static displays created by Hmisc or in SAS. 18 | 19 | ## Typical Usage 20 | Generally speaking, no configuration is needed to create a web-codebook. Just [load a JSON data set](https://github.com/RhoInc/web-codebook/wiki/Data-Guidelines) and the tool will automatically create a user interface (filters, etc.) based on the data set loaded. Initialize the chart like so: 21 | 22 | ```javascript 23 | webcodebook.createChart('#chartLocation', {}).init(data); 24 | ``` 25 | 26 | See the [API](https://github.com/RhoInc/web-codebook/wiki/API) and [Configuration](https://github.com/RhoInc/web-codebook/wiki/Codebook-Configuration) pages for more details about custom configurations. 27 | 28 | ## Links 29 | 30 | - [Interactive Codebook Example](https://rhoinc.github.io/web-codebook/test-page/default/) 31 | - [Interactive Explorer Example](https://rhoinc.github.io/web-codebook/test-page/explorer/) 32 | - [Configuration](https://github.com/RhoInc/web-codebook/wiki/Codebook-Configuration) 33 | - [API](https://github.com/RhoInc/web-codebook/wiki/API) 34 | - [Technical Documentation](https://github.com/RhoInc/web-codebook/wiki/Technical-Documentation) 35 | - [Data Guidelines](https://github.com/RhoInc/web-codebook/wiki/Data-Guidelines) 36 | - [datadigest R Package](https://github.com/RhoInc/datadigest) 37 | 38 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /css/webcodebook.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,300); 2 | 3 | /*------------------------------------------------------------------------------------------------\ 4 | General 5 | \------------------------------------------------------------------------------------------------*/ 6 | 7 | .web-codebook { 8 | position: relative; 9 | width: 1024px; 10 | font-family: 'Open Sans', Helvetica, Arial, sans-serif; 11 | z-index: 10; 12 | } 13 | 14 | .web-codebook > * { 15 | display: block; 16 | width: 100%; 17 | } 18 | 19 | .web-codebook .hidden { 20 | display: none !important; 21 | } 22 | 23 | .web-codebook #loading-indicator { 24 | position: absolute; 25 | height: 100%; 26 | z-index: 9999; 27 | background-color: rgba(255, 255, 255, 0.9); 28 | } 29 | 30 | .web-codebook #loading-indicator .spinner { 31 | position: absolute; 32 | top: 128px; 33 | left: 480px; 34 | border: 12.5px solid #008cba; 35 | border-top: 12.5px solid #999; 36 | border-radius: 50%; 37 | width: 64px; 38 | height: 64px; 39 | -webkit-animation: spin 2s linear infinite; 40 | animation: spin 2s linear infinite; 41 | z-index: 9999; 42 | } 43 | 44 | @-webkit-keyframes spin { 45 | 0% { 46 | -webkit-transform: rotate(0deg); 47 | } 48 | 49 | 100% { 50 | -webkit-transform: rotate(360deg); 51 | } 52 | } 53 | 54 | @keyframes spin { 55 | 0% { 56 | transform: rotate(0deg); 57 | } 58 | 59 | 100% { 60 | transform: rotate(360deg); 61 | } 62 | } 63 | 64 | .web-codebook > .section { 65 | margin-top: 0.5em; 66 | } 67 | 68 | .web-codebook ul { 69 | padding: 0; 70 | margin: 0; 71 | } 72 | 73 | .web-codebook .clear-highlight { 74 | margin-right: 1em; 75 | } 76 | 77 | /*------------------------------------------------------------------------------------------------\ 78 | Status section 79 | \------------------------------------------------------------------------------------------------*/ 80 | 81 | .web-codebook .status { 82 | padding: .1em; 83 | margin: .5em 0 .5em 0; 84 | border: 1px solid transparent; 85 | border-radius: 4px; 86 | color: #3c763d; 87 | background-color: #dff0d8; 88 | border-color: #d6e9c6; 89 | border-bottom: 1px dotted #999; 90 | } 91 | 92 | .web-codebook .status.error { 93 | color: #a94442; 94 | background-color: #f2dede; 95 | border-color: #ebccd1; 96 | } 97 | 98 | /*------------------------------------------------------------------------------------------------\ 99 | Instructions and Title layout 100 | \------------------------------------------------------------------------------------------------*/ 101 | 102 | .web-codebook .wcb-nav { 103 | margin-top: 0.1em; 104 | } 105 | 106 | .web-codebook .title .file { 107 | font-weight: 500; 108 | font-size: 1.4em; 109 | } 110 | 111 | .web-codebook .title .countSummary { 112 | padding-left: 1em; 113 | font-size: 0.8em; 114 | color: #666; 115 | } 116 | 117 | .web-codebook .title .countSummary.warn { 118 | color: red; 119 | } 120 | 121 | .web-codebook .highlightCount { 122 | font-size: 0.8em; 123 | color: #666; 124 | } 125 | 126 | .web-codebook .instructions .control { 127 | padding-left: 0.5em; 128 | } 129 | 130 | .web-codebook span.highlightLegend { 131 | background-color: orange; 132 | } 133 | 134 | /*------------------------------------------------------------------------------------------------\ 135 | Controls layout 136 | \------------------------------------------------------------------------------------------------*/ 137 | 138 | .web-codebook .controls { 139 | position: relative; 140 | display: inline-block; 141 | vertical-align: top; 142 | border: 1px dashed #ccc; 143 | margin-bottom: 0.5em; 144 | overflow: hidden; 145 | } 146 | 147 | .web-codebook .controls > * { 148 | margin: .25em .5em; 149 | } 150 | 151 | .web-codebook .controls .controls-title { 152 | font-weight: bold; 153 | padding-bottom: 1em; 154 | } 155 | 156 | .web-codebook .controls .data-listing-toggle { 157 | display: inline-block; 158 | } 159 | 160 | .web-codebook .controls .group-select span { 161 | padding-right: 5px; 162 | } 163 | 164 | .web-codebook .controls .group-select, 165 | .web-codebook .controls .chart-toggle { 166 | display: inline-block; 167 | padding-right: 1em; 168 | } 169 | 170 | .web-codebook .controls button, 171 | .web-codebook .instructions button, 172 | .web-codebook .title button { 173 | margin-right: 0.5em; 174 | background-color: white; 175 | border: 2px solid #008cba; 176 | border-radius: 4px; 177 | padding: .2em .4em; 178 | } 179 | 180 | .web-codebook .controls button:hover, 181 | .web-codebook .instructions button:hover, 182 | .web-codebook .title button:hover { 183 | cursor: pointer; 184 | background-color: #008cba; 185 | color: white; 186 | } 187 | 188 | .web-codebook .controls button.control-toggle { 189 | position: absolute; 190 | top: 0; 191 | right: 0; 192 | margin-top: 0.5em; 193 | } 194 | 195 | .web-codebook .controls span.update-controls { 196 | position: absolute; 197 | bottom: 0; 198 | right: 0; 199 | margin-top: 0.5em; 200 | margin-right: 0.25em; 201 | font-size: 150%; 202 | cursor: pointer; 203 | } 204 | 205 | .web-codebook .controls span.update-controls:hover { 206 | font-weight: bold; 207 | } 208 | 209 | .web-codebook .controls .custom-filters ul li { 210 | margin: 0 3px 5px 0; 211 | } 212 | 213 | .web-codebook .controls .custom-filters ul li.active > a > label, 214 | .web-codebook .controls .custom-filters ul li:hover > a > label { 215 | color: white; 216 | } 217 | 218 | .web-codebook .controls .custom-filters ul li .filterType { 219 | font-size: 0.8em; 220 | } 221 | 222 | .web-codebook .controls .custom-filters ul li.filterCustom { 223 | display: inline-block; 224 | padding-right: 5px; 225 | } 226 | 227 | .web-codebook .controls .custom-filters ul li.filterCustom * { 228 | display: block; 229 | min-width: 50px; 230 | } 231 | 232 | .web-codebook .controls .custom-filters ul li.filterCustom span.filterLabel * { 233 | display: inline-block; 234 | } 235 | 236 | .web-codebook .controls .custom-filters ul li.filterCustom span.filter-label { 237 | display: block; 238 | font-size: .75em; 239 | color: #777; 240 | margin-bottom: 3px; 241 | } 242 | 243 | 244 | 245 | /*------------------------------------------------------------------------------------------------\ 246 | Row layout 247 | \------------------------------------------------------------------------------------------------*/ 248 | 249 | 250 | 251 | .web-codebook .summaryTable .summaryText { 252 | display: block; 253 | clear: both; 254 | } 255 | 256 | .web-codebook .summaryTable .variable-row { 257 | position: relative; 258 | display: block; 259 | border-bottom: 1px solid #ccc; 260 | } 261 | 262 | .web-codebook .summaryTable .variable-row .section { 263 | display: block; 264 | padding-left: 0.2em; 265 | } 266 | 267 | .web-codebook .summaryTable .variable-row.hiddenDetails .row-chart, 268 | .web-codebook .summaryTable .variable-row.hiddenDetails .row-details, 269 | .web-codebook .summaryTable .variable-row.hiddenDetails .row-meta { 270 | display: none; 271 | } 272 | 273 | /**-------------------------------------------------------------------------------------------\ 274 | Header layout 275 | \-------------------------------------------------------------------------------------------**/ 276 | 277 | .web-codebook .summaryTable .variable-row .row-head { 278 | background: #eee; 279 | padding: 0.1em; 280 | overflow: hidden; 281 | } 282 | 283 | .web-codebook .summaryTable .variable-row .row-head > * { 284 | display: inline-block; 285 | } 286 | 287 | /**-------------------------------------------------------------------------------------------\ 288 | Header layout - Toggle 289 | \-------------------------------------------------------------------------------------------**/ 290 | .web-codebook .summaryTable .variable-row .row-title { 291 | padding-right: 1em; 292 | width: 100%; 293 | } 294 | 295 | .web-codebook .summaryTable .variable-row .row-title > * { 296 | display: inline-block; 297 | } 298 | 299 | .web-codebook .summaryTable .variable-row .row-title .row-toggle { 300 | font-size: 0.8em; 301 | cursor: pointer; 302 | vertical-align: top; 303 | color: #999; 304 | padding-right: 0.5em; 305 | } 306 | 307 | .web-codebook .summaryTable .variable-row .row-title .row-toggle:hover { 308 | background: #eee; 309 | } 310 | 311 | /**-------------------------------------------------------------------------------------------\ 312 | Header - Title layout 313 | \-------------------------------------------------------------------------------------------**/ 314 | .web-codebook .summaryTable .variable-row .row-title .title-span { 315 | font-weight: bold; 316 | } 317 | 318 | .web-codebook .summaryTable .variable-row .row-title .label-span { 319 | padding-left: 0.2em; 320 | font-size: 0.8em; 321 | } 322 | 323 | /**-------------------------------------------------------------------------------------------\ 324 | Header - Type Tag 325 | \-------------------------------------------------------------------------------------------**/ 326 | .web-codebook .summaryTable .variable-row .row-title .type { 327 | background-color: white; 328 | border: 1px solid black; 329 | border-radius: 2px; 330 | padding: 2px 4px 2px 4px; 331 | margin-left: 0.4em; 332 | font-size: 0.6em; 333 | vertical-align: middle; 334 | float: right; 335 | } 336 | 337 | .web-codebook .summaryTable .variable-row .row-title .spark { 338 | float: right; 339 | } 340 | 341 | .web-codebook .summaryTable .variable-row .row-title .spark .sparkLabel { 342 | font-size: 0.5em; 343 | padding: 0 0.2em; 344 | margin-left: 0.5em; 345 | margin-bottom: 0.4em; 346 | margin-right: 0.5em; 347 | display: inline-block; 348 | width: 3em; 349 | border-radius: 0.2em; 350 | vertical-align: middle; 351 | text-align: center; 352 | background: white; 353 | color: #999; 354 | cursor: help; 355 | } 356 | 357 | /**-------------------------------------------------------------------------------------------\ 358 | Header - Missing Count 359 | \-------------------------------------------------------------------------------------------**/ 360 | .web-codebook .summaryTable .variable-row .row-title .percent-missing { 361 | margin-right: 0.4em; 362 | font-size: 0.8em; 363 | vertical-align: middle; 364 | float: right; 365 | } 366 | 367 | 368 | /**-------------------------------------------------------------------------------------------\ 369 | Row Details - layout 370 | \-------------------------------------------------------------------------------------------**/ 371 | .web-codebook .summaryTable .variable-row .row-details { 372 | border-top: 1px solid #ccc; 373 | border-bottom: 1px solid #ccc; 374 | padding-bottom: 0.2em; 375 | } 376 | 377 | .web-codebook .summaryTable .variable-row .row-details ul { 378 | display: inline-block; 379 | } 380 | 381 | .web-codebook .summaryTable .variable-row .row-details ul.values { 382 | float: right; 383 | } 384 | 385 | .web-codebook .summaryTable .variable-row .row-details ul li { 386 | display: inline-block; 387 | vertical-align: top; 388 | } 389 | 390 | .web-codebook .summaryTable .variable-row .row-details ul li div { 391 | display: block; 392 | text-align: left; 393 | width: 100%; 394 | padding-right: .4em; 395 | } 396 | 397 | .web-codebook .summaryTable .variable-row .row-details ul li div.wcb-label { 398 | display: inline-block; 399 | font-size: 0.65em; 400 | color: #999; 401 | min-width: 1em; 402 | max-width: 100px; 403 | white-space: nowrap; 404 | overflow: hidden; 405 | text-overflow: ellipsis; 406 | } 407 | 408 | .web-codebook .summaryTable .variable-row .row-details ul li div.value { 409 | font-size: 0.85em; 410 | color: #333; 411 | vertical-align: bottom; 412 | } 413 | 414 | .web-codebook .summaryTable .variable-row .row-details ul li div.details { 415 | padding-right: .2em; 416 | font-size: 0.5em; 417 | cursor: pointer; 418 | color: blue; 419 | } 420 | 421 | 422 | 423 | /**-------------------------------------------------------------------------------------------\ 424 | Chart layout 425 | \-------------------------------------------------------------------------------------------**/ 426 | 427 | .web-codebook .summaryTable .variable-row .row-controls > * { 428 | display: inline-block; 429 | margin-right: .5em; 430 | } 431 | 432 | .web-codebook .summaryTable .variable-row .row-chart .missingText { 433 | padding: .5em; 434 | color: #f03b20; 435 | font-size: 0.8em; 436 | } 437 | 438 | .web-codebook .summaryTable .variable-row .row-chart div.characterSummary { 439 | padding: .5em; 440 | } 441 | 442 | .web-codebook .summaryTable .variable-row .row-chart div.characterSummary span.caution { 443 | font-size: 0.8em; 444 | color: #f03b20; 445 | } 446 | 447 | .web-codebook .summaryTable .variable-row .row-chart div.characterSummary span.caution span.drawLevel { 448 | text-decoration: underline; 449 | cursor: pointer; 450 | } 451 | 452 | .web-codebook .summaryTable .variable-row .row-chart p.panel-label { 453 | margin-top: 0.2em; 454 | margin-bottom: 0.2em; 455 | } 456 | 457 | .web-codebook .summaryTable g.bar-group.highlighted rect { 458 | fill: orange; 459 | } 460 | 461 | .web-codebook .summaryTable g.bar-group rect:hover { 462 | cursor: pointer; 463 | stroke-width: 3px; 464 | stroke: black; 465 | } 466 | 467 | .web-codebook .summaryTable .variable-row .row-chart .legend { 468 | margin-left: 1em; 469 | } 470 | 471 | .web-codebook .summaryTable .variable-row .row-chart .svg-tooltip { 472 | display: none; 473 | } 474 | 475 | .web-codebook .summaryTable .variable-row .row-chart .svg-tooltip.active { 476 | display: block; 477 | } 478 | 479 | /**-------------------------------------------------------------------------------------------\ 480 | Metadata - layout 481 | \-------------------------------------------------------------------------------------------**/ 482 | .web-codebook .summaryTable .variable-row .row-meta { 483 | border-top: 1px solid #ccc; 484 | padding-bottom: 0.2em; 485 | } 486 | 487 | .web-codebook .summaryTable .variable-row .row-meta ul { 488 | display: inline-block; 489 | } 490 | 491 | .web-codebook .summaryTable .variable-row .row-meta ul li { 492 | display: inline-block; 493 | background-color: white; 494 | border: 1px solid black; 495 | border-radius: 2px; 496 | padding: 2px 4px 2px 4px; 497 | margin-left: 0.4em; 498 | font-size: 0.6em; 499 | } 500 | 501 | .web-codebook .summaryTable .variable-row .row-meta ul li.details { 502 | border: none; 503 | } 504 | 505 | .web-codebook .summaryTable .variable-row .row-meta ul li div { 506 | display: inline-block; 507 | text-align: left; 508 | margin: .1em; 509 | vertical-align: middle; 510 | } 511 | 512 | .web-codebook .summaryTable .variable-row .row-meta ul li div.wcb-label { 513 | display: inline-block; 514 | font-size: 0.65em; 515 | color: #999; 516 | min-width: 1em; 517 | max-width: 100px; 518 | white-space: nowrap; 519 | overflow: hidden; 520 | text-overflow: ellipsis; 521 | vertical-align: middle; 522 | } 523 | 524 | .web-codebook .summaryTable .variable-row .row-meta ul li div.value { 525 | display: inline-block; 526 | font-size: 0.85em; 527 | color: #333; 528 | } 529 | 530 | /*------------------------------------------------------------------------------------------------\ 531 | Data listing 532 | \------------------------------------------------------------------------------------------------*/ 533 | 534 | .web-codebook .dataListing .description { 535 | padding: 0 5px 0 5px; 536 | } 537 | 538 | .web-codebook .dataListing tbody tr.highlight { 539 | background: orange !important; 540 | } 541 | 542 | 543 | /***--------------------------------------------------------------------------------------\ 544 | Nav 545 | \--------------------------------------------------------------------------------------***/ 546 | 547 | .web-codebook .wcb-nav ul.wcb-nav-tabs { 548 | list-style-type: none; 549 | overflow: hidden; 550 | background-color: black; 551 | padding: 0; 552 | } 553 | 554 | .web-codebook .wcb-nav ul.wcb-nav-tabs li { 555 | float: left; 556 | cursor: pointer; 557 | border: 2px solid black; 558 | height: 2.5em; 559 | padding: 0 10px 0 10px; 560 | line-height: 2.5em; 561 | text-align: center; 562 | } 563 | 564 | .web-codebook .wcb-nav ul.wcb-nav-tabs li a { 565 | display: block; 566 | text-align: center; 567 | color: white; 568 | text-decoration: none; 569 | } 570 | 571 | .web-codebook .wcb-nav ul.wcb-nav-tabs li:hover { 572 | background-color: #777; 573 | border: 2px solid #777; 574 | } 575 | 576 | .web-codebook .wcb-nav ul.wcb-nav-tabs li.settings { 577 | float: right; 578 | } 579 | 580 | .web-codebook .wcb-nav ul.wcb-nav-tabs li.active, 581 | ul.wcb-nav-tabs li.active:hover { 582 | background-color: white; 583 | border: 2px solid black; 584 | } 585 | 586 | .web-codebook .wcb-nav ul.wcb-nav-tabs li.active a, 587 | ul.wcb-nav-tabs li.active:hover a { 588 | color: black; 589 | } 590 | 591 | /*------------------------------------------------------------------------------------------------\ 592 | Settings 593 | \------------------------------------------------------------------------------------------------*/ 594 | 595 | .web-codebook .settings table.column-table { 596 | border-collapse: collapse; 597 | } 598 | 599 | .web-codebook .settings table.column-table thead tr { 600 | border-bottom: 2px solid black; 601 | } 602 | 603 | .web-codebook .settings table.column-table tbody tr { 604 | border-bottom: 1px dotted #ccc; 605 | } 606 | 607 | .web-codebook .settings table.column-table th { 608 | text-align: center; 609 | padding: 2px 10px; 610 | } 611 | 612 | .web-codebook .settings table.column-table th.Column { 613 | text-align: right; 614 | } 615 | 616 | .web-codebook .settings table.column-table td { 617 | text-align: center; 618 | padding: 2px 10px; 619 | } 620 | 621 | .web-codebook .settings table.column-table td.Column { 622 | text-align: right; 623 | } 624 | 625 | /*------------------------------------------------------------------------------------------------\ 626 | Data Listing 627 | \------------------------------------------------------------------------------------------------*/ 628 | 629 | .web-codebook .fileListing tr.selected { 630 | background: #008cba; 631 | color: white; 632 | } 633 | 634 | .web-codebook .fileListing td.link { 635 | color: blue; 636 | text-decoration: underline; 637 | cursor: pointer; 638 | } 639 | 640 | /*------------------------------------------------------------------------------------------------\ 641 | Data Listing - Data Loader 642 | \------------------------------------------------------------------------------------------------*/ 643 | 644 | .web-codebook .fileListing .dataLoader { 645 | padding-bottom: 1em; 646 | } 647 | 648 | .web-codebook .fileListing .dataLoader label { 649 | font-size: 0.8em; 650 | font-weight: 0; 651 | border-bottom: 1px solid black; 652 | display: inline-block; 653 | padding: 0.1em; 654 | cursor: pointer; 655 | padding-right: 0.5em; 656 | } 657 | 658 | .web-codebook .fileListing .dataLoader label:hover { 659 | background-color: #ccc; 660 | } 661 | 662 | .web-codebook .fileListing .dataLoader label input { 663 | display: none; 664 | } 665 | 666 | /*------------------------------------------------------------------------------------------------\ 667 | Chart Maker 668 | \------------------------------------------------------------------------------------------------*/ 669 | 670 | .web-codebook .chartMaker.section .cm-controls .control { 671 | padding-right: .5em; 672 | } 673 | 674 | .web-codebook .chartMaker.section .cm-chart .wc-small-multiples .wc-chart { 675 | border: 1px solid #999; 676 | padding: 0.2em; 677 | margin: 0.2em; 678 | } 679 | 680 | .web-codebook .chartMaker.section .cm-chart .wc-small-multiples .wc-chart .wc-chart-title { 681 | background: #eee; 682 | padding: 0.1em; 683 | font-size: 1.2em; 684 | } 685 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webcodebook", 3 | "version": "1.7.1", 4 | "description": "Interactive data set summaries", 5 | "keywords": [ 6 | "data", 7 | "summary", 8 | "interactive", 9 | "codebook" 10 | ], 11 | "homepage": "https://github.com/rhoinc/web-codebook", 12 | "license": "MIT", 13 | "author": "Rho, Inc.", 14 | "main": "./build/webcodebook.js", 15 | "module": "./src/index.js", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/rhoinc/web-codebook.git" 19 | }, 20 | "dependencies": { 21 | "d3": "~3", 22 | "webcharts": "^1.10.0" 23 | }, 24 | "scripts": { 25 | "build": "npm audit fix && npm run bundle && npm run format", 26 | "bundle": "rollup -c", 27 | "format": "npm run format-src && npm run format-bundle && npm run format-test && npm run format-css", 28 | "format-bundle": "prettier --single-quote --write ./build/webcodebook.js", 29 | "format-css": "stylefmt ./css/webcodebook.css", 30 | "format-src": "prettier --single-quote --write \"./src/**/*.js\"", 31 | "format-test": "prettier --single-quote --write \"./test-page/**/*.js\"", 32 | "test-page": "start chrome ./test-page/index.html && start firefox ./test-page/index.html && start iexplore file://%CD%/test-page/index.html", 33 | "watch": "rollup -c -w" 34 | }, 35 | "devDependencies": { 36 | "babel-plugin-external-helpers": "^6.22.0", 37 | "babel-preset-env": "^1.7.0", 38 | "babel-register": "^6.26.0", 39 | "prettier": "^1.14.3", 40 | "rollup": "^0.66.6", 41 | "rollup-plugin-babel": "^3.0.7", 42 | "stylefmt": "^6.0.3" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/rhoinc/web-codebook/issues" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | 3 | var pkg = require('./package.json'); 4 | 5 | module.exports = { 6 | input: pkg.module, 7 | output: { 8 | name: pkg.name 9 | .split('-') 10 | .map((str,i) => 11 | i === 0 ? 12 | str : 13 | (str.substring(0,1).toUpperCase() + str.substring(1)) 14 | ) 15 | .join(''), 16 | file: pkg.main, 17 | format: 'umd', 18 | globals: { 19 | d3: 'd3', 20 | webcharts: 'webCharts' 21 | }, 22 | }, 23 | external: (function() { 24 | var dependencies = pkg.dependencies; 25 | 26 | return Object.keys(dependencies); 27 | }()), 28 | plugins: [ 29 | babel({ 30 | exclude: 'node_modules/**', 31 | presets: [ 32 | [ 'env', {modules: false} ] 33 | ], 34 | plugins: [ 35 | 'external-helpers' 36 | ], 37 | babelrc: false 38 | }) 39 | ] 40 | }; 41 | -------------------------------------------------------------------------------- /settings-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "", 3 | "overview": "The most straightforward way to customize a codebook is to define a settings object whose properties describe the codebook's behavior and appearance. The settings object can be passed as the second argument to `webcodebook.createCodebook()` (or its alias `webcodebook.createChart()`). All defaults can be overwritten by users when creating a codebook. Parameters for the settings object are described in detail below.", 4 | "version": "1.7.1", 5 | "type": "object", 6 | "properties": { 7 | "filters": { 8 | "type": "array", 9 | "title": "Filters", 10 | "description": "an array of filter variables; by default variables with 10 or fewer levels become filters", 11 | "items": { 12 | "type": "object", 13 | "properties": { 14 | "value_col": { 15 | "type": "string", 16 | "title": "Variable Name", 17 | "description": "the name of the filter variable" 18 | }, 19 | "label": { 20 | "type": "string", 21 | "title": "Variable Label", 22 | "description": "a label that describes the filter variable" 23 | } 24 | } 25 | } 26 | }, 27 | "groups": { 28 | "type": "array", 29 | "title": "Groups", 30 | "description": "an array of group variables; by default variables with 5 or fewer levels become group options", 31 | "items": { 32 | "type": "object", 33 | "properties": { 34 | "value_col": { 35 | "type": "string", 36 | "title": "Variable Name", 37 | "description": "the name of the group variable" 38 | }, 39 | "label": { 40 | "type": "string", 41 | "title": "Variable Label", 42 | "description": "a label that describes the group variable" 43 | } 44 | } 45 | } 46 | }, 47 | "variableLabels": { 48 | "type": "array", 49 | "title": "Variable Labels", 50 | "description": "an array of variable objects with both the variable name and a brief description of the variable", 51 | "items": { 52 | "type": "object", 53 | "properties": { 54 | "value_col": { 55 | "type": "string", 56 | "title": "Variable Name", 57 | "description": "the name of the variable" 58 | }, 59 | "label": { 60 | "type": "string", 61 | "title": "Variable Label", 62 | "description": "a label that describes the variable" 63 | } 64 | } 65 | } 66 | }, 67 | "variableTypes": { 68 | "type": "array", 69 | "title": "Variable Type", 70 | "description": "an array of variable objects with both the variable name and the type of the variable", 71 | "items": { 72 | "type": "object", 73 | "properties": { 74 | "value_col": { 75 | "type": "string", 76 | "title": "Variable Name", 77 | "description": "the name of the variable" 78 | }, 79 | "type": { 80 | "type": "string", 81 | "title": "Variable Type", 82 | "description": "a label that describes the variable", 83 | "enum": [ 84 | "continuous", 85 | "categorical" 86 | ] 87 | 88 | } 89 | } 90 | } 91 | }, 92 | "hiddenVariables": { 93 | "type": "array", 94 | "title": "Hidden Variables", 95 | "description": "an array of variables that will be hidden throughout the codebook", 96 | "items": { 97 | "type": "string" 98 | } 99 | }, 100 | "meta": { 101 | "type": "array", 102 | "title": "Variable Metadata", 103 | "description": "an array of variable metadata", 104 | "items": { 105 | "type": "object", 106 | "properties": { 107 | "value_col": { 108 | "type": "string", 109 | "title": "Variable Name", 110 | "description": "the name of the variable" 111 | }, 112 | "label": { 113 | "type": "string", 114 | "title": "Variable Label", 115 | "description": "a label that describes the variable" 116 | }, 117 | "filter": { 118 | "type": "boolean", 119 | "title": "Filter?", 120 | "description": "includes variable as a filter" 121 | }, 122 | "group": { 123 | "type": "boolean", 124 | "title": "Group?", 125 | "description": "includes variable as a group option" 126 | }, 127 | "type": { 128 | "type": "string", 129 | "title": "Variable Type", 130 | "description": "the type of data contained in the variable" 131 | } 132 | } 133 | } 134 | }, 135 | "autogroups": { 136 | "type": "number", 137 | "title": "Level Cutoff to Choose Group Variables", 138 | "description": "the number of levels in a variable over which the variable will not be added as a group option", 139 | "default": 5 140 | }, 141 | "autofilter": { 142 | "type": "number", 143 | "title": "Level Cutoff to Choose Filter Variables", 144 | "description": "the number of levels in a variable over which the variable will not be added as a filter", 145 | "default": 10 146 | }, 147 | "autobins": { 148 | "type": "boolean", 149 | "title": "Automatically Choose Number of Bins in Histogram?", 150 | "description": "the number of bins in histograms will be determined algorithmically based on the range of the variable and the number of observations", 151 | "default": true 152 | }, 153 | "nBins": { 154 | "type": "number", 155 | "title": "Number of Bins in Histogram", 156 | "description": "the number of bins in which to split out each continuous variable into", 157 | "default": 100 158 | }, 159 | "levelSplit": { 160 | "type": "number", 161 | "title": "Level Cutoff to Choose Horizontal or Vertical Bar Charts", 162 | "description": "the number of levels in a variable over which the variable will be summarized with a vertical bar chart as opposed to a horizontal bar chart", 163 | "default": 5 164 | }, 165 | "controlVisibility": { 166 | "type": "string", 167 | "title": "Control Visibility", 168 | "description": "the initial state of the display of the controls", 169 | "default": "visible", 170 | "enum": [ 171 | "hidden", 172 | "minimized", 173 | "visible" 174 | ] 175 | }, 176 | "chartVisibility": { 177 | "type": "string", 178 | "title": "Chart Visibility", 179 | "description": "the initial state of the display of the charts", 180 | "default": "minimized", 181 | "enum": [ 182 | "hidden", 183 | "minimized", 184 | "visible" 185 | ] 186 | }, 187 | "tabs": { 188 | "type": "array", 189 | "title": "Codebook Tabs", 190 | "description": "an array of the tabs displayed in the codebook", 191 | "default": [ 192 | "codebook", 193 | "listing", 194 | "chartMaker", 195 | "settings" 196 | ], 197 | "items": { 198 | "type": "string", 199 | "enum": [ 200 | "codebook", 201 | "listing", 202 | "chartMaker", 203 | "settings", 204 | "files" 205 | ] 206 | } 207 | }, 208 | "dataName": { 209 | "type": "string", 210 | "title": "Data Name", 211 | "description": "the name of the data file summarized in the codebook", 212 | "default": "" 213 | }, 214 | "whiteSpaceAsMissing": { 215 | "type": "boolean", 216 | "title": "White space as missing", 217 | "description": "White space will be treated as missing values", 218 | "default": true 219 | }, 220 | "missingValues": { 221 | "type": "array", 222 | "title": "missing Values", 223 | "description": "Array of missing values. Note that json schema does not support the default value of [null, NaN, undefined]" 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/charts.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define controls object. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { createVerticalBars } from './charts/createVerticalBars'; 6 | import { createVerticalBarsControls } from './charts/createVerticalBarsControls'; 7 | import { createHorizontalBars } from './charts/createHorizontalBars'; 8 | import { createHorizontalBarsControls } from './charts/createHorizontalBarsControls'; 9 | import { createHistogramBoxPlot } from './charts/createHistogramBoxPlot'; 10 | import { createHistogramBoxPlotControls } from './charts/createHistogramBoxPlotControls'; 11 | import { createDotPlot } from './charts/createDotPlot'; 12 | 13 | export const charts = { 14 | createVerticalBars: createVerticalBars, 15 | createVerticalBarsControls: createVerticalBarsControls, 16 | createHorizontalBars: createHorizontalBars, 17 | createHorizontalBarsControls: createHorizontalBarsControls, 18 | createHistogramBoxPlot: createHistogramBoxPlot, 19 | createHistogramBoxPlotControls: createHistogramBoxPlotControls, 20 | createDotPlot: createDotPlot 21 | }; 22 | -------------------------------------------------------------------------------- /src/charts/createDotPlot.js: -------------------------------------------------------------------------------- 1 | import clone from '../util/clone'; 2 | import onResize from './dotPlot/onResize'; 3 | import { createChart } from 'webcharts'; 4 | import { select as d3select } from 'd3'; 5 | 6 | export function createDotPlot(this_, d) { 7 | const rowSelector = d3select(this_).node().parentNode, 8 | outcome = d3select(rowSelector) 9 | .select('.row-controls .x-axis-outcome select') 10 | .property('value'), 11 | chartContainer = d3select(this_).node(), 12 | chartSettings = { 13 | x: { 14 | column: outcome === 'rate' ? 'prop_n' : 'n', 15 | type: 'linear', 16 | label: '', 17 | format: outcome === 'rate' ? '%' : 'd', 18 | domain: [0, null] 19 | }, 20 | y: { 21 | column: 'key', 22 | type: 'ordinal', 23 | label: '' 24 | }, 25 | marks: [ 26 | { 27 | type: 'circle', 28 | per: ['key'], 29 | summarizeX: 'mean', 30 | tooltip: '[key]: [n] ([prop_n_text])' 31 | } 32 | ], 33 | gridlines: 'xy', 34 | resizable: false, 35 | height: this_.height, 36 | margin: this_.margin, 37 | value_col: d.value_col, 38 | group_col: d.group || null, 39 | group_label: d.groupLabel || null, 40 | overall: d.statistics.values, 41 | chartType: d.chartType 42 | }, 43 | chartData = d.statistics.values 44 | .sort( 45 | (a, b) => 46 | a.prop_n > b.prop_n 47 | ? -2 48 | : a.prop_n < b.prop_n 49 | ? 2 50 | : a.key < b.key 51 | ? -1 52 | : 1 53 | ) 54 | .slice(0, 5); // sort data by descending rate and keep only the first five categories. 55 | 56 | chartSettings.y.order = chartData.map(d => d.key).reverse(); 57 | 58 | if (d.groups) { 59 | //Define overall data. 60 | chartData.forEach(di => (di.group = 'Overall')); 61 | 62 | //Add group data to overall data. 63 | d.groups.forEach(group => { 64 | group.statistics.values 65 | .filter(value => chartSettings.y.order.indexOf(value.key) > -1) 66 | .sort( 67 | (a, b) => 68 | a.prop_n > b.prop_n 69 | ? -2 70 | : a.prop_n < b.prop_n 71 | ? 2 72 | : a.key < b.key 73 | ? -1 74 | : 1 75 | ) 76 | .forEach(value => { 77 | value.group = group.group; 78 | chartData.push(value); 79 | }); 80 | }); 81 | 82 | chartSettings.marks[0].per.push('group'); 83 | 84 | //Overall mark 85 | if (outcome === 'rate') { 86 | chartSettings.marks[0].values = { group: ['Overall'] }; 87 | 88 | //Group marks 89 | chartSettings.marks[1] = clone(chartSettings.marks[0]); 90 | chartSettings.marks[1].values = { group: d.groups.map(d => d.group) }; 91 | } 92 | 93 | chartSettings.color_by = 'group'; 94 | chartSettings.legend = { 95 | label: '', 96 | order: d.groups.map(d => d.group), 97 | mark: 'circle' 98 | }; 99 | } 100 | 101 | const chart = createChart(chartContainer, chartSettings); 102 | chart.on('resize', onResize); 103 | chart.init( 104 | chartData.filter(d => !(outcome === 'frequency' && d.group === 'Overall')) 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /src/charts/createHistogramBoxPlot.js: -------------------------------------------------------------------------------- 1 | import { defineHistogram } from './histogramBoxPlot/defineHistogram'; 2 | import { select as d3select } from 'd3'; 3 | 4 | export function createHistogramBoxPlot(this_, d) { 5 | const chartContainer = d3select(this_).node(); 6 | const chartSettings = { 7 | measure: ' ', 8 | resizable: false, 9 | height: 100, 10 | margin: this_.margin, 11 | nBins: d.bins, 12 | chartType: d.chartType, 13 | commonScale: d.commonScale == undefined ? true : d.commonScale 14 | }; 15 | let chartData = []; 16 | 17 | if (d.groups) { 18 | chartSettings.panel = 'group'; 19 | chartSettings.group_col = d.group; 20 | chartSettings.group_label = d.groupLabel; 21 | d.groups.forEach(group => { 22 | group.values.forEach(value => { 23 | chartData.push({ 24 | group: group.group || '', 25 | ' ': value.value, 26 | index: value.index, 27 | highlighted: value.highlighted 28 | }); 29 | }); 30 | }); 31 | } else { 32 | d.values.forEach(d => { 33 | chartData.push({ 34 | ' ': d.value, 35 | index: d.index, 36 | highlighted: d.highlighted 37 | }); 38 | }); 39 | } 40 | 41 | const chart = defineHistogram(chartContainer, chartSettings); 42 | chart.init(chartData); 43 | } 44 | -------------------------------------------------------------------------------- /src/charts/createHistogramBoxPlotControls.js: -------------------------------------------------------------------------------- 1 | import { createHistogramBoxPlot } from './createHistogramBoxPlot.js'; 2 | import { select as d3select } from 'd3'; 3 | 4 | export function createHistogramBoxPlotControls(this_, d) { 5 | const controlsContainer = d3select(this_) 6 | .append('div') 7 | .classed('row-controls', true); 8 | 9 | //add control for commonScale control (only if data is grouped) 10 | if (d.group) { 11 | var commonScaleWrap = controlsContainer 12 | .append('div') 13 | .classed('common-scale-control', true); 14 | commonScaleWrap.append('small').text('Standardize axes across panels? '); 15 | var commonScaleCheckbox = commonScaleWrap 16 | .append('input') 17 | .attr('type', 'checkbox') 18 | .attr('checked', true); 19 | 20 | commonScaleCheckbox.on('change', function() { 21 | d3select(this_) 22 | .selectAll('.wc-chart') 23 | .remove(); 24 | d3select(this_) 25 | .selectAll('.panel-label') 26 | .remove(); 27 | d.commonScale = this.checked; 28 | createHistogramBoxPlot(this_, d); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/charts/createHorizontalBars.js: -------------------------------------------------------------------------------- 1 | import clone from '../util/clone'; 2 | import onInit from './horizontalBars/onInit'; 3 | import onResize from './horizontalBars/onResize'; 4 | import { createChart } from 'webcharts'; 5 | import { select as d3select, max as d3max, merge as d3merge } from 'd3'; 6 | 7 | export function createHorizontalBars(this_, d) { 8 | const rowSelector = d3select(this_).node().parentNode, 9 | outcome = d3select(rowSelector) 10 | .select('.row-controls .x-axis-outcome select') 11 | .property('value'), 12 | custom_height = d.statistics.values.length * 20 + 35, // let height vary based on the number of levels; 35 ~= top and bottom margin 13 | chartContainer = d3select(this_).node(), 14 | chartSettings = { 15 | x: { 16 | column: outcome === 'rate' ? 'prop_n' : 'n', 17 | type: 'linear', 18 | label: '', 19 | format: outcome === 'rate' ? '%' : 'd', 20 | domain: [0, null] 21 | }, 22 | y: { 23 | column: 'key', 24 | type: 'ordinal', 25 | label: '' 26 | }, 27 | marks: [ 28 | { 29 | type: 'bar', 30 | per: ['key'], 31 | tooltip: '[key]: [n] ([prop_n_text])', 32 | attributes: { 33 | stroke: null 34 | } 35 | } 36 | ], 37 | colors: ['#999'], 38 | gridlines: 'x', 39 | resizable: false, 40 | height: custom_height, 41 | margin: this_.margin, 42 | value_col: d.value_col, 43 | group_col: d.group || null, 44 | group_label: d.groupLabel || null, 45 | overall: d.statistics.values, 46 | chartType: d.chartType 47 | }; 48 | var chartData = d.statistics.values.sort( 49 | (a, b) => 50 | a.prop_n > b.prop_n 51 | ? -2 52 | : a.prop_n < b.prop_n 53 | ? 2 54 | : a.key < b.key 55 | ? -1 56 | : 1 57 | ); // sort data by descending rate and keep only the first five categories. 58 | 59 | chartSettings.y.order = chartData.map(d => d.key).reverse(); 60 | 61 | //Add highlight values (if any) 62 | chartData.forEach(function(d) { 63 | d.type = 'Main'; 64 | }); 65 | 66 | if (d.statistics.highlightValues) { 67 | d.statistics.highlightValues.forEach(function(d) { 68 | d.type = 'sub'; 69 | }); 70 | chartData = d3merge([chartData, d.statistics.highlightValues]); 71 | 72 | chartSettings.marks[0].per = ['key', 'type']; 73 | chartSettings.marks[0].arrange = 'nested'; 74 | chartSettings.color_by = 'type'; 75 | chartSettings.colors = ['#999', 'orange']; 76 | } 77 | 78 | if (d.groups) { 79 | //Set upper limit of x-axis domain to the maximum group rate. 80 | chartSettings.x.domain[1] = d3max(d.groups, di => 81 | d3max(di.statistics.values, dii => dii[chartSettings.x.column]) 82 | ); 83 | 84 | d.groups.forEach(group => { 85 | //Define group-level settings. 86 | group.chartSettings = clone(chartSettings); 87 | group.chartSettings.group_val = group.group; 88 | group.chartSettings.n = group.values.length; 89 | 90 | //Sort data by descending rate and keep only the first five categories. 91 | group.data = group.statistics.values 92 | .filter(di => chartSettings.y.order.indexOf(di.key) > -1) 93 | .sort( 94 | (a, b) => 95 | a.prop_n > b.prop_n 96 | ? -2 97 | : a.prop_n < b.prop_n 98 | ? 2 99 | : a.key < b.key 100 | ? -1 101 | : 1 102 | ); 103 | 104 | group.data.forEach(function(d) { 105 | d.type = 'main'; 106 | }); 107 | if (group.statistics.highlightValues) { 108 | group.statistics.highlightValues.forEach(function(d) { 109 | d.type = 'sub'; 110 | }); 111 | group.data = d3merge([group.data, group.statistics.highlightValues]); 112 | 113 | group.chartSettings.marks[0].per = ['key', 'type']; 114 | group.chartSettings.marks[0].arrange = 'nested'; 115 | group.chartSettings.color_by = 'type'; 116 | group.chartSettings.colors = ['#999', 'orange']; 117 | } 118 | 119 | //Define chart. 120 | group.chart = createChart(chartContainer, group.chartSettings); 121 | group.chart.on('init', onInit); 122 | group.chart.on('resize', onResize); 123 | 124 | if (group.data.length) group.chart.init(group.data); 125 | else { 126 | d3select(chartContainer) 127 | .append('p') 128 | .text( 129 | `${chartSettings.group_col}: ${group.chartSettings.group_val} (n=${ 130 | group.chartSettings.n 131 | })` 132 | ); 133 | d3select(chartContainer) 134 | .append('div') 135 | .html(`All values missing in this group..

`); 136 | } 137 | }); 138 | } else { 139 | //Define chart. 140 | const chart = createChart(chartContainer, chartSettings); 141 | chart.on('init', onInit); 142 | chart.on('resize', onResize); 143 | chart.init(chartData); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/charts/createHorizontalBarsControls.js: -------------------------------------------------------------------------------- 1 | import { createHorizontalBars } from './createHorizontalBars.js'; 2 | import { createDotPlot } from './createDotPlot.js'; 3 | import { select as d3select } from 'd3'; 4 | 5 | export function createHorizontalBarsControls(this_, d) { 6 | const controlsContainer = d3select(this_) 7 | .append('div') 8 | .classed('row-controls', true); 9 | 10 | //add control that changes y-axis scale 11 | var outcomes = ['rate', 'frequency']; 12 | var outcomeWrap = controlsContainer 13 | .append('div') 14 | .classed('x-axis-outcome', true); 15 | outcomeWrap.append('small').text('Summarize by: '); 16 | var outcomeSelect = outcomeWrap.append('select'); 17 | outcomeSelect 18 | .selectAll('option') 19 | .data(outcomes) 20 | .enter() 21 | .append('option') 22 | .text(d => d); 23 | 24 | outcomeSelect.on('change', function() { 25 | d3select(this_) 26 | .selectAll('.wc-chart') 27 | .remove(); 28 | d3select(this_) 29 | .selectAll('.panel-label') 30 | .remove(); 31 | if (type_control.property('value') === 'Paneled (Bar Charts)') { 32 | createHorizontalBars(this_, d); 33 | } else { 34 | createDotPlot(this_, d); 35 | } 36 | }); 37 | 38 | //add control that change chart type 39 | var chart_type_values = ['Paneled (Bar Charts)', 'Grouped (Dot Plot)']; 40 | var chartTypeWrap = controlsContainer 41 | .append('div') 42 | .classed('chart-type', true) 43 | .classed('hidden', !d.groups); // hide the controls if the chart isn't Grouped 44 | chartTypeWrap.append('small').text('Display Type: '); 45 | var type_control = chartTypeWrap.append('select'); 46 | type_control 47 | .selectAll('option') 48 | .data(chart_type_values) 49 | .enter() 50 | .append('option') 51 | .text(d => d); 52 | 53 | type_control.on('change', function() { 54 | d3select(this_) 55 | .selectAll('.wc-chart') 56 | .remove(); 57 | d3select(this_) 58 | .selectAll('.panel-label') 59 | .remove(); 60 | if (this.value == 'Paneled (Bar Charts)') { 61 | createHorizontalBars(this_, d); 62 | } else { 63 | createDotPlot(this_, d); 64 | } 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /src/charts/createSpark.js: -------------------------------------------------------------------------------- 1 | import { select } from 'd3'; 2 | import makeHist from './spark/makeHist'; 3 | 4 | export default function createSpark() { 5 | var d = select(this).datum(); 6 | if (d.statistics.n > 0) { 7 | makeHist(this, d); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/charts/createVerticalBars.js: -------------------------------------------------------------------------------- 1 | import clone from '../util/clone'; 2 | import onResize from './verticalBars/onResize'; 3 | import onInit from './verticalBars/onInit'; 4 | import axisSort from './verticalBars/axisSort'; 5 | import { createChart } from 'webcharts'; 6 | import { select as d3select, max as d3max, merge as d3merge } from 'd3'; 7 | 8 | export function createVerticalBars(this_, d) { 9 | const chartContainer = d3select(this_).node(); 10 | const rowSelector = d3select(this_).node().parentNode; 11 | var sortType = d3select(rowSelector) 12 | .select('.row-controls .x-axis-sort select') 13 | .property('value'); 14 | var outcome = d3select(rowSelector) 15 | .select('.row-controls .y-axis-outcome select') 16 | .property('value'); 17 | const chartSettings = { 18 | y: { 19 | column: outcome === 'rate' ? 'prop_n' : 'n', 20 | type: 'linear', 21 | label: '', 22 | format: outcome === 'rate' ? '0.1%' : 'd', 23 | domain: [0, null] 24 | }, 25 | x: { 26 | column: 'key', 27 | type: 'ordinal', 28 | label: '' 29 | }, 30 | marks: [ 31 | { 32 | type: 'bar', 33 | per: ['key'], 34 | attributes: { 35 | stroke: null 36 | } 37 | } 38 | ], 39 | colors: ['#999'], 40 | gridlines: 'y', 41 | resizable: false, 42 | height: this_.height, 43 | margin: this_.margin, 44 | value_col: d.value_col, 45 | group_col: d.group || null, 46 | group_label: d.groupLabel || null, 47 | overall: d.statistics.values, 48 | sort: sortType, //Alphabetical, Ascending, Descending 49 | chartType: d.chartType 50 | }; 51 | 52 | chartSettings.margin.bottom = 10; 53 | 54 | var chartData = d.statistics.values.sort(function(a, b) { 55 | return axisSort(a, b, chartSettings.sort); 56 | }); 57 | chartSettings.x.order = chartData.map(d => d.key); 58 | var x_dom = chartData.map(d => d.key); 59 | 60 | //Add highlight values (if any) 61 | chartData.forEach(function(d) { 62 | d.type = 'Main'; 63 | }); 64 | 65 | if (d.statistics.highlightValues) { 66 | d.statistics.highlightValues.forEach(function(d) { 67 | d.type = 'sub'; 68 | }); 69 | chartData = d3merge([chartData, d.statistics.highlightValues]); 70 | 71 | chartSettings.marks[0].per = ['key', 'type']; 72 | chartSettings.marks[0].arrange = 'nested'; 73 | chartSettings.color_by = 'type'; 74 | chartSettings.colors = ['#999', 'orange']; 75 | } 76 | 77 | if (d.groups) { 78 | //Set upper limit of y-axis domain to the maximum group rate. 79 | chartSettings.y.domain[1] = d3max(d.groups, di => 80 | d3max(di.statistics.values, dii => dii[chartSettings.y.column]) 81 | ); 82 | 83 | chartSettings.x.domain = x_dom; //use the overall x domain in paneled charts 84 | d.groups.forEach(group => { 85 | //Define group-level settings. 86 | group.chartSettings = clone(chartSettings); 87 | group.chartSettings.group_val = group.group; 88 | group.chartSettings.n = group.values.length; 89 | group.data = group.statistics.values; 90 | group.data.forEach(function(d) { 91 | d.type = 'main'; 92 | }); 93 | if (group.statistics.highlightValues) { 94 | group.statistics.highlightValues.forEach(function(d) { 95 | d.type = 'sub'; 96 | }); 97 | group.data = d3merge([group.data, group.statistics.highlightValues]); 98 | 99 | group.chartSettings.marks[0].per = ['key', 'type']; 100 | group.chartSettings.marks[0].arrange = 'nested'; 101 | group.chartSettings.color_by = 'type'; 102 | group.chartSettings.colors = ['#999', 'orange']; 103 | } 104 | 105 | //Define chart. 106 | group.chart = createChart(chartContainer, group.chartSettings); 107 | group.chart.on('init', onInit); 108 | group.chart.on('resize', onResize); 109 | 110 | if (group.data.length) group.chart.init(group.data); 111 | else { 112 | d3select(chartContainer) 113 | .append('p') 114 | .text( 115 | `${chartSettings.group_col}: ${group.chartSettings.group_val} (n=${ 116 | group.chartSettings.n 117 | })` 118 | ); 119 | 120 | d3select(chartContainer) 121 | .append('div') 122 | .html(`No data available for this level..

`); 123 | } 124 | }); 125 | } else { 126 | //Define chart. 127 | const chart = createChart(chartContainer, chartSettings); 128 | chart.on('init', onInit); 129 | chart.on('resize', onResize); 130 | chart.init(chartData); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/charts/createVerticalBarsControls.js: -------------------------------------------------------------------------------- 1 | import { createVerticalBars } from './createVerticalBars.js'; 2 | import { select as d3select } from 'd3'; 3 | 4 | export function createVerticalBarsControls(this_, d) { 5 | const controlsContainer = d3select(this_) 6 | .append('div') 7 | .classed('row-controls', true); 8 | 9 | //add control that changes y-axis scale 10 | var outcomes = ['rate', 'frequency']; 11 | var outcomeWrap = controlsContainer 12 | .append('div') 13 | .classed('y-axis-outcome', true); 14 | outcomeWrap.append('small').text('Summarize by: '); 15 | var outcomeSelect = outcomeWrap.append('select'); 16 | outcomeSelect 17 | .selectAll('option') 18 | .data(outcomes) 19 | .enter() 20 | .append('option') 21 | .text(d => d); 22 | 23 | outcomeSelect.on('change', function() { 24 | d3select(this_) 25 | .selectAll('.wc-chart') 26 | .remove(); 27 | d3select(this_) 28 | .selectAll('.panel-label') 29 | .remove(); 30 | createVerticalBars(this_, d); 31 | }); 32 | 33 | //add control that changes x-axis order 34 | var sort_values = ['Alphabetical', 'Ascending', 'Descending']; 35 | var sortWrap = controlsContainer.append('div').classed('x-axis-sort', true); 36 | sortWrap.append('small').text('Sort levels: '); 37 | var x_sort = sortWrap.append('select'); 38 | x_sort 39 | .selectAll('option') 40 | .data(sort_values) 41 | .enter() 42 | .append('option') 43 | .text(d => d); 44 | 45 | x_sort.on('change', function() { 46 | d3select(this_) 47 | .selectAll('.wc-chart') 48 | .remove(); 49 | d3select(this_) 50 | .selectAll('.panel-label') 51 | .remove(); 52 | createVerticalBars(this_, d); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /src/charts/dotPlot/drawOverallMark.js: -------------------------------------------------------------------------------- 1 | import { format as d3format } from 'd3'; 2 | 3 | export default function drawOverallMark(chart) { 4 | //Clear overall marks. 5 | chart.svg.selectAll('.overall-mark').remove(); 6 | 7 | //For each mark draw an overall mark. 8 | chart.config.overall.forEach(d => { 9 | if (chart.config.y.order.indexOf(d.key) > -1) { 10 | const g = chart.svg.append('g').classed('overall-mark', true); 11 | const x = d.prop_n; 12 | const y = d.key; 13 | 14 | //Draw vertical line representing the overall rate of the current categorical value. 15 | if (chart.y(y)) { 16 | const rateLine = g 17 | .append('line') 18 | .attr({ 19 | x1: chart.x(x), 20 | y1: chart.y(y), 21 | x2: chart.x(x), 22 | y2: chart.y(y) + chart.y.rangeBand() 23 | }) 24 | .style({ 25 | stroke: 'black', 26 | 'stroke-width': '2px', 27 | 'stroke-opacity': '1' 28 | }); 29 | rateLine.append('title').text(`Overall rate: ${d3format('.1%')(x)}`); 30 | } 31 | } 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/charts/dotPlot/modifyOverallLegendMark.js: -------------------------------------------------------------------------------- 1 | export default function modifyOverallLegendMark(chart) { 2 | const legendItems = chart.wrap.selectAll('.legend-item'), 3 | overallMark = legendItems.filter(d => d.label === 'Overall').select('svg'), 4 | BBox = overallMark.node().getBBox(); 5 | overallMark.select('.legend-mark').remove(); 6 | overallMark 7 | .append('line') 8 | .classed('legend-mark', true) 9 | .attr({ 10 | x1: (3 * BBox.width) / 4, 11 | y1: 0, 12 | x2: (3 * BBox.width) / 4, 13 | y2: BBox.height 14 | }) 15 | .style({ 16 | stroke: 'black', 17 | 'stroke-width': '2px', 18 | 'stroke-opacity': '1' 19 | }); 20 | legendItems.selectAll('circle').attr('r', '.4em'); 21 | } 22 | -------------------------------------------------------------------------------- /src/charts/dotPlot/moveYaxis.js: -------------------------------------------------------------------------------- 1 | export default function moveYaxis(chart) { 2 | const ticks = chart.wrap.selectAll('g.y.axis g.tick'); 3 | ticks.select('text').remove(); 4 | ticks.append('title').text(d => d); 5 | ticks 6 | .append('text') 7 | .attr({ 8 | 'text-anchor': 'start', 9 | 'alignment-baseline': 'middle', 10 | dx: '1em', 11 | x: chart.plot_width 12 | }) 13 | .text(d => (d.length < 30 ? d : d.substring(0, 30) + '...')); 14 | } 15 | -------------------------------------------------------------------------------- /src/charts/dotPlot/onResize.js: -------------------------------------------------------------------------------- 1 | import moveYaxis from './moveYaxis'; 2 | import drawOverallMark from './drawOverallMark'; 3 | import modifyOverallLegendMark from './modifyOverallLegendMark'; 4 | 5 | export default function onResize() { 6 | moveYaxis(this); 7 | if (this.config.x.column === 'prop_n') { 8 | drawOverallMark(this); 9 | if (this.config.color_by) modifyOverallLegendMark(this); 10 | 11 | //Hide overall dots. 12 | if (this.config.color_by) this.svg.selectAll('.Overall').remove(); 13 | else this.svg.selectAll('.point').remove(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/charts/histogramBoxPlot/addBoxPlot.js: -------------------------------------------------------------------------------- 1 | import { 2 | format as d3format, 3 | quantile as d3quantile, 4 | mean as d3mean, 5 | deviation as d3deviation, 6 | mouse as d3mouse 7 | } from 'd3'; 8 | export default function addBoxPlot(chart) { 9 | const format = d3format(chart.config.measureFormat); 10 | 11 | //Annotate quantiles 12 | if (chart.config.boxPlot) { 13 | const quantiles = [ 14 | { probability: 0.05, label: '5th percentile' }, 15 | { probability: 0.25, label: '1st quartile' }, 16 | { probability: 0.5, label: 'Median' }, 17 | { probability: 0.75, label: '3rd quartile' }, 18 | { probability: 0.95, label: '95th percentile' } 19 | ]; 20 | 21 | for (const item in quantiles) { 22 | const quantile = quantiles[item]; 23 | quantile.quantile = d3quantile(chart.values, quantile.probability); 24 | 25 | //Horizontal lines 26 | if ([0.05, 0.75].indexOf(quantile.probability) > -1) { 27 | const rProbability = quantiles[+item + 1].probability; 28 | const rQuantile = d3quantile(chart.values, rProbability); 29 | const whisker = chart.svg 30 | .append('line') 31 | .attr({ 32 | class: 'statistic', 33 | x1: chart.x(quantile.quantile), 34 | y1: chart.plot_height + chart.config.boxPlotHeight / 2, 35 | x2: chart.x(rQuantile), 36 | y2: chart.plot_height + chart.config.boxPlotHeight / 2 37 | }) 38 | .style({ 39 | stroke: 'black', 40 | 'stroke-width': '2px', 41 | opacity: 0.25 42 | }); 43 | whisker 44 | .append('title') 45 | .text( 46 | `Q${quantile.probability}-Q${rProbability}: ${format( 47 | quantile.quantile 48 | )}-${format(rQuantile)}` 49 | ); 50 | } 51 | 52 | //Box 53 | if (quantile.probability === 0.25) { 54 | const q3 = d3quantile(chart.values, 0.75); 55 | const interQ = chart.svg 56 | .append('rect') 57 | .attr({ 58 | class: 'statistic', 59 | x: chart.x(quantile.quantile), 60 | y: chart.plot_height, 61 | width: chart.x(q3) - chart.x(quantile.quantile), 62 | height: chart.config.boxPlotHeight 63 | }) 64 | .style({ 65 | fill: '#ccc', 66 | opacity: 0.25 67 | }); 68 | interQ 69 | .append('title') 70 | .text( 71 | `Interquartile range: ${format(quantile.quantile)}-${format(q3)}` 72 | ); 73 | } 74 | 75 | //Vertical lines 76 | quantile.mark = chart.svg 77 | .append('line') 78 | .attr({ 79 | class: 'statistic', 80 | x1: chart.x(quantile.quantile), 81 | y1: chart.plot_height, 82 | x2: chart.x(quantile.quantile), 83 | y2: chart.plot_height + chart.config.boxPlotHeight 84 | }) 85 | .style({ 86 | stroke: 87 | [0.05, 0.95].indexOf(quantile.probability) > -1 88 | ? 'black' 89 | : [0.25, 0.75].indexOf(quantile.probability) > -1 90 | ? 'black' 91 | : 'black', 92 | 'stroke-width': '3px' 93 | }); 94 | quantile.mark 95 | .append('title') 96 | .text(`${quantile.label}: ${format(quantile.quantile)}`); 97 | } 98 | 99 | var outliers = chart.values.filter(function(f) { 100 | var low_outlier = 101 | f < 102 | quantiles.filter(q => { 103 | if (q.probability == 0.05) { 104 | return q; 105 | } 106 | })[0]['quantile']; 107 | var high_outlier = 108 | f > 109 | quantiles.filter(q => { 110 | if (q.probability == 0.95) { 111 | return q; 112 | } 113 | })[0]['quantile']; 114 | return low_outlier || high_outlier; 115 | }); 116 | 117 | if (outliers.length < 100) { 118 | chart.svg 119 | .selectAll('line.outlier') 120 | .data(outliers) 121 | .enter() 122 | .append('line') 123 | .attr('class', 'outlier') 124 | .attr('x1', d => chart.x(d)) 125 | .attr('x2', d => chart.x(d)) 126 | .attr('y1', d => chart.plot_height * 1.07) 127 | .attr( 128 | 'y2', 129 | d => (chart.plot_height + chart.config.boxPlotHeight) / 1.07 130 | ) 131 | .style({ 132 | fill: '#000000', 133 | stroke: 'black', 134 | 'stroke-width': '1px' 135 | }); 136 | } else { 137 | console.log( 138 | `${outliers.length} outliers not drawn for the following chart:` 139 | ); 140 | console.log(chart.wrap); 141 | } 142 | } 143 | 144 | //Annotate mean. 145 | if (chart.config.mean) { 146 | const mean = d3mean(chart.values); 147 | const sd = d3deviation(chart.values); 148 | const meanMark = chart.svg 149 | .append('circle') 150 | .attr({ 151 | class: 'statistic', 152 | cx: chart.x(mean), 153 | cy: chart.plot_height + chart.config.boxPlotHeight / 2, 154 | r: chart.config.boxPlotHeight / 3 155 | }) 156 | .style({ 157 | fill: '#000000', 158 | stroke: 'black', 159 | 'stroke-width': '1px' 160 | }); 161 | meanMark 162 | .append('title') 163 | .text( 164 | `n: ${chart.values.length}\nMean: ${format(mean)}\nSD: ${format(sd)}` 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/charts/histogramBoxPlot/addHighlightMarks.js: -------------------------------------------------------------------------------- 1 | import { sum as d3sum, select as d3select } from 'd3'; 2 | 3 | export default function addHighlightMarks(chart) { 4 | //add highlights for each bar (if any exist) 5 | var bars = chart.svg.selectAll('g.bar-group').each(function(d) { 6 | var highlightCount = d3sum(d.values.raw, function(d) { 7 | return d.highlighted ? 1 : 0; 8 | }); 9 | //Clone the rect (if there are highlights) 10 | if (highlightCount > 0) { 11 | var rect = d3select(this).select('rect'); 12 | var rectNode = rect.node(); 13 | var highlightRect = d3select(this).append('rect'); 14 | 15 | highlightRect 16 | .attr('x', chart.x(d.rangeLow) + 1) 17 | .attr('y', chart.y(highlightCount)) 18 | .attr('height', chart.y(0) - chart.y(highlightCount)) 19 | .attr('width', chart.x(d.rangeHigh) - 1 - (chart.x(d.rangeLow) + 1)) 20 | .attr('fill', 'orange'); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/charts/histogramBoxPlot/addModals.js: -------------------------------------------------------------------------------- 1 | import { mouse as d3mouse } from 'd3'; 2 | 3 | export default function addModals(chart) { 4 | const bars = chart.svg.selectAll('.bar-group'); 5 | const tooltips = chart.svg.selectAll('.svg-tooltip'); 6 | const statistics = chart.svg.selectAll('.statistic'); 7 | chart.svg 8 | .on('mousemove', function() { 9 | //Highlight closest bar. 10 | const mouse = d3mouse(this); 11 | const x = chart.x.invert(mouse[0]); 12 | const y = chart.y.invert(mouse[1]); 13 | let minimum; 14 | let bar = {}; 15 | bars.each(function(d, i) { 16 | d.distance = Math.abs(d.midpoint - x); 17 | if (i === 0 || d.distance < minimum) { 18 | minimum = d.distance; 19 | bar = d; 20 | } 21 | }); 22 | const closest = bars 23 | .filter(d => d.distance === minimum) 24 | .filter((d, i) => i === 0) 25 | .select('rect'); 26 | bars.select('rect').style('stroke-width', '1px'); 27 | closest.style('stroke-width', '3px'); 28 | 29 | //Activate tooltip. 30 | const d = closest.datum(); 31 | tooltips.classed('active', false); 32 | chart.svg.select('#' + d.selector).classed('active', true); 33 | }) 34 | .on('mouseout', function() { 35 | bars.select('rect').style('stroke-width', '1px'); 36 | chart.svg.selectAll('g.svg-tooltip').classed('active', false); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/charts/histogramBoxPlot/defaultSettings.js: -------------------------------------------------------------------------------- 1 | import clone from '../../util/clone'; 2 | 3 | export default //Custom settings 4 | { 5 | measure: null, 6 | panel: null, 7 | measureFormat: ',.2f', 8 | boxPlot: true, 9 | nBins: null, 10 | mean: true, 11 | overall: false, 12 | boxPlotHeight: 20, 13 | commonScale: true, 14 | //Webcharts settings 15 | x: { 16 | column: null, // set in syncSettings() 17 | type: 'linear', 18 | label: '', 19 | bin: null 20 | }, // set in syncSettings() 21 | y: { 22 | column: null, // set in syncSettings() 23 | type: 'linear', 24 | label: '', 25 | domain: [0, null] 26 | }, 27 | marks: [ 28 | { 29 | type: 'bar', 30 | per: null, // set in syncSettings() 31 | summarizeX: 'mean', 32 | summarizeY: 'count', 33 | attributes: { 34 | fill: '#999', 35 | stroke: '#333', 36 | 'stroke-width': '1px' 37 | } 38 | } 39 | ], 40 | gridlines: 'y', 41 | resizable: true, 42 | aspect: 12, 43 | margin: { 44 | right: 25, 45 | left: 100 46 | } // space for panel value 47 | }; 48 | 49 | //Replicate settings in multiple places in the settings object. 50 | export function syncSettings(settings) { 51 | const syncedSettings = clone(settings); 52 | 53 | if (syncedSettings.panel === null) syncedSettings.overall = true; 54 | syncedSettings.x.column = settings.measure; 55 | syncedSettings.x.bin = settings.nBins; 56 | syncedSettings.y.column = settings.measure; 57 | syncedSettings.y.label = settings.measure; 58 | syncedSettings.marks[0].per = [settings.measure]; 59 | syncedSettings.margin.bottom = settings.boxPlotHeight + 20; 60 | return syncedSettings; 61 | } 62 | -------------------------------------------------------------------------------- /src/charts/histogramBoxPlot/defineHistogram.js: -------------------------------------------------------------------------------- 1 | import clone from '../../util/clone'; 2 | import defaultSettings, { syncSettings } from './defaultSettings'; 3 | 4 | import { createChart } from 'webcharts'; 5 | 6 | import onInit from './onInit'; 7 | import onResize from './onResize'; 8 | 9 | export function defineHistogram(element, settings) { 10 | //Merge specified settings with default settings. 11 | const mergedSettings = Object.assign({}, defaultSettings, settings); 12 | 13 | //Sync properties within merged settings. 14 | const syncedSettings = syncSettings(mergedSettings); 15 | 16 | //Sync control inputs with merged settings. 17 | //let syncedControlInputs = syncControlInputs(controlInputs, mergedSettings); 18 | //let controls = createControls(element, {location: 'top', inputs: syncedControlInputs}); 19 | 20 | //Define chart. 21 | const chart = createChart(element, syncedSettings); // Add third argument to define controls as needed. 22 | chart.initialSettings = clone(syncedSettings); 23 | chart.initialSettings.container = element; 24 | chart.on('init', onInit); 25 | chart.on('resize', onResize); 26 | 27 | return chart; 28 | } 29 | -------------------------------------------------------------------------------- /src/charts/histogramBoxPlot/makeTooltip.js: -------------------------------------------------------------------------------- 1 | import { format as d3format } from 'd3'; 2 | 3 | export default function makeTooltip(d, i, context) { 4 | const format = d3format(context.config.measureFormat), 5 | offset = context.plot_width / context.config.x.bin / 2 + 8; 6 | d.midpoint = (d.rangeHigh + d.rangeLow) / 2; 7 | d.range = `${format(d.rangeLow)}-${format(d.rangeHigh)}`; 8 | d.selector = `bar` + i; 9 | d.side = context.x(d.midpoint) < context.plot_width / 2 ? 'left' : 'right'; 10 | d.xPosition = 11 | d.side === 'left' 12 | ? context.x(d.midpoint) + offset 13 | : context.x(d.midpoint) - offset; 14 | 15 | //Define tooltips. 16 | const tooltip = context.svg.append('g').attr('id', d.selector), 17 | text = tooltip.append('text').attr({ 18 | id: 'text', 19 | x: d.xPosition, 20 | y: context.plot_height, 21 | dy: '-.75em', 22 | 'font-size': '75%', 23 | 'font-weight': 'bold', 24 | fill: 'white' 25 | }); 26 | text 27 | .append('tspan') 28 | .attr({ 29 | x: d.xPosition, 30 | 'text-anchor': d.side === 'left' ? 'start' : 'end' 31 | }) 32 | .text(`Range: ${d.range}`); 33 | text 34 | .append('tspan') 35 | .attr({ 36 | x: d.xPosition, 37 | dy: '-1.5em', 38 | 'text-anchor': d.side === 'left' ? 'start' : 'end' 39 | }) 40 | .text(`n: ${d.total}`); 41 | const dimensions = text[0][0].getBBox(); 42 | tooltip.classed('svg-tooltip', true); //have to run after .getBBox() in FF/EI since this sets display:none 43 | 44 | const background = tooltip 45 | .append('rect') 46 | .attr({ 47 | id: 'background', 48 | x: dimensions.x - 5, 49 | y: dimensions.y - 2, 50 | width: dimensions.width + 10, 51 | height: dimensions.height + 4 52 | }) 53 | .style({ 54 | fill: 'black', 55 | stroke: 'white' 56 | }); 57 | tooltip[0][0].insertBefore(background[0][0], text[0][0]); 58 | } 59 | -------------------------------------------------------------------------------- /src/charts/histogramBoxPlot/moveXaxis.js: -------------------------------------------------------------------------------- 1 | export default function moveXaxis(chart) { 2 | var xticks = chart.svg.select('.x.axis').selectAll('g.tick'); 3 | xticks.select('text').remove(); 4 | xticks 5 | .append('text') 6 | .attr('y', chart.config.boxPlotHeight) 7 | .attr('dy', '1em') 8 | .attr('x', 0) 9 | .attr('text-anchor', 'middle') 10 | .attr('alignment-baseline', 'top') 11 | .text(d => d); 12 | } 13 | -------------------------------------------------------------------------------- /src/charts/histogramBoxPlot/moveYaxis.js: -------------------------------------------------------------------------------- 1 | export default function moveYaxis(chart) { 2 | const ticks = chart.wrap.selectAll('g.y.axis g.tick'); 3 | ticks.select('text').remove(); 4 | ticks.append('title').text(d => d); 5 | ticks 6 | .append('text') 7 | .attr({ 8 | 'text-anchor': 'start', 9 | 'alignment-baseline': 'middle', 10 | dx: '.5em', 11 | x: chart.plot_width 12 | }) 13 | .text(d => d); 14 | } 15 | -------------------------------------------------------------------------------- /src/charts/histogramBoxPlot/onInit.js: -------------------------------------------------------------------------------- 1 | import clone from '../../util/clone'; 2 | import onResize from './onResize'; 3 | import { createChart } from 'webcharts'; 4 | import { 5 | extent as d3extent, 6 | nest as d3nest, 7 | max as d3max, 8 | set as d3set 9 | } from 'd3'; 10 | 11 | export default function onInit() { 12 | const context = this; 13 | const config = this.initialSettings; 14 | const measure = config.measure; 15 | const panel = config.panel; 16 | 17 | //Add a label 18 | if (this.group) { 19 | const groupTitle = this.wrap 20 | .append('p') 21 | .attr('class', 'panel-label') 22 | .style('margin-left', context.config.margin.left + 'px') 23 | .html( 24 | `${this.config.group_col}: ${this.group} (n=${ 25 | this.raw_data.length 26 | })` 27 | ); 28 | this.wrap 29 | .node() 30 | .parentNode.insertBefore(groupTitle.node(), this.wrap.node()); 31 | } 32 | 33 | //Remove non-numeric and missing values. 34 | if (!this.group) { 35 | this.initialSettings.unfilteredData = this.raw_data; 36 | this.raw_data = this.initialSettings.unfilteredData.filter( 37 | d => !isNaN(+d[measure]) && !/^\s*$/.test(d[measure]) 38 | ); 39 | } 40 | 41 | //Create array of values. 42 | this.values = this.raw_data.map(d => +d[measure]).sort((a, b) => a - b); 43 | 44 | //Define x-axis domain as the range of the measure, regardless of subgrouping. 45 | if (!this.initialSettings.xDomain) { 46 | this.initialSettings.xDomain = d3extent(this.values); 47 | } 48 | this.config.x.domain = this.initialSettings.xDomain; 49 | 50 | /**-------------------------------------------------------------------------------------------\ 51 | Paneling 52 | \-------------------------------------------------------------------------------------------**/ 53 | 54 | if (panel && !this.group) { 55 | //Nest data by paneling variable to efine y-axis domain as the maximum number of observations 56 | //in a single bin within a subgrouping. 57 | let max = 0; 58 | if (!config.y.domain[1]) { 59 | const nestedData = d3nest() 60 | .key(d => d[panel]) 61 | .entries(context.raw_data); 62 | nestedData.forEach(group => { 63 | const domain = d3extent(group.values, d => +d[measure]); 64 | const binWidth = (domain[1] - domain[0]) / config.nBins; 65 | group.values.forEach(d => { 66 | d.bin = 67 | Math.floor((+d[measure] - domain[0]) / binWidth) - 68 | (+d[measure] === domain[1]) * 1; 69 | }); 70 | const bins = d3nest() 71 | .key(d => d.bin) 72 | .rollup(d => d.length) 73 | .entries(group.values); 74 | max = Math.max(max, d3max(bins, d => d.values)); 75 | }); 76 | } 77 | 78 | //Plot the chart for each group. 79 | const groups = d3set(context.raw_data.map(d => d[panel])) 80 | .values() 81 | .map(d => { 82 | return { group: d }; 83 | }) 84 | .sort((a, b) => (a.group < b.group ? -1 : 1)); 85 | 86 | groups.forEach((group, i) => { 87 | group.settings = clone(config); 88 | group.settings.y.label = group.group; 89 | group.settings.y.domain = config.commonScale ? [0, max] : [0, null]; 90 | group.data = context.raw_data.filter(d => d[panel] === group.group); 91 | group.settings.xDomain = config.commonScale 92 | ? config.xDomain 93 | : d3extent(group.data, d => +d[measure]); 94 | group.settings.x.domain = group.settings.xDomain; 95 | group.webChart = new createChart(config.container, group.settings); 96 | group.webChart.initialSettings = group.settings; 97 | group.webChart.group = group.group; 98 | group.webChart.on('init', onInit); 99 | group.webChart.on('resize', onResize); 100 | group.webChart.init(group.data); 101 | }); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/charts/histogramBoxPlot/onResize.js: -------------------------------------------------------------------------------- 1 | import makeTooltip from './makeTooltip'; 2 | import moveYaxis from './moveYaxis'; 3 | import moveXaxis from './moveXaxis'; 4 | import highlightData from '../util/highlightData.js'; 5 | import addHighlightMarks from './addHighlightMarks.js'; 6 | import addBoxPlot from './addBoxPlot.js'; 7 | import addModals from './addModals.js'; 8 | 9 | export default function onResize() { 10 | const context = this; 11 | 12 | //Hide overall plot if [settings.overall] is set to false. 13 | if (!this.config.overall && !this.group) { 14 | this.wrap.style('display', 'none'); 15 | this.wrap.classed('overall', true); 16 | } else { 17 | //Clear custom marks. 18 | this.svg.selectAll('g.svg-tooltip').remove(); 19 | this.svg.selectAll('.statistic').remove(); 20 | 21 | //Add boxPlot 22 | addBoxPlot(this); 23 | 24 | //Create tooltips 25 | this.svg.selectAll('g.bar-group').each(function(d, i) { 26 | makeTooltip(d, i, context); 27 | }); 28 | 29 | this.svg.select('g.y.axis text.axis-title').remove(); //Remove y-axis label 30 | this.wrap.select('ul.legend').remove(); //Hide legends. 31 | moveXaxis(this); //Shift x-axis tick labels downward. 32 | addModals(this); //Add modal to nearest mark. 33 | } 34 | 35 | moveYaxis(this); //Move Y axis to the right 36 | highlightData(this); //Add event listener to marks to highlight data. 37 | addHighlightMarks(this); //add new rects for highlight marks (if any) 38 | } 39 | -------------------------------------------------------------------------------- /src/charts/horizontalBars/drawDifferences.js: -------------------------------------------------------------------------------- 1 | import { format as d3format, select as d3select } from 'd3'; 2 | 3 | export default function drawDifferences(chart) { 4 | //Clear difference marks and annotations. 5 | chart.svg.selectAll('.difference-from-total').remove(); 6 | 7 | //For each mark draw a difference mark and annotation. 8 | chart.current_data 9 | .filter(function(d) { 10 | return d.values.raw[0].type == 'main'; 11 | }) 12 | .forEach(d => { 13 | const overall = chart.config.overall.filter(function(di) { 14 | return di.key === d.values.raw[0].key; 15 | })[0], 16 | g = chart.svg 17 | .append('g') 18 | .classed('difference-from-total', true) 19 | .style('display', 'none'), 20 | x = overall[chart.config.x.column], 21 | y = overall.key; 22 | 23 | //Draw line from overall rate to group rate. 24 | const diffLine = g 25 | .append('line') 26 | .attr({ 27 | x1: chart.x(x), 28 | y1: chart.y(y) + chart.y.rangeBand() / 2, 29 | x2: chart.x(d.total), 30 | y2: chart.y(y) + chart.y.rangeBand() / 2 31 | }) 32 | .style({ 33 | stroke: 'black', 34 | 'stroke-width': '2px', 35 | 'stroke-opacity': '.25' 36 | }); 37 | diffLine 38 | .append('title') 39 | .text( 40 | `Difference from overall rate: ${d3format('.1f')( 41 | (d.total - x) * 100 42 | )}` 43 | ); 44 | const diffText = g 45 | .append('text') 46 | .attr({ 47 | x: chart.x(d.total), 48 | y: chart.y(y) + chart.y.rangeBand() / 2, 49 | dx: x < d.total ? '5px' : '-2px', 50 | 'text-anchor': x < d.total ? 'beginning' : 'end', 51 | 'font-size': '0.7em' 52 | }) 53 | .text( 54 | `${x < d.total ? '+' : x > d.total ? '-' : ''}${d3format('.1f')( 55 | Math.abs(d.total - x) * 100 56 | )}` 57 | ); 58 | }); 59 | 60 | //Display difference from total on hover. 61 | chart.svg 62 | .on('mouseover', () => { 63 | chart.svg.selectAll('.difference-from-total').style('display', 'block'); 64 | chart.svg.selectAll('.difference-from-total text').each(function() { 65 | d3select(this).attr('dy', this.getBBox().height / 4); 66 | }); 67 | }) 68 | .on('mouseout', () => 69 | chart.svg.selectAll('.difference-from-total').style('display', 'none') 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/charts/horizontalBars/drawOverallMark.js: -------------------------------------------------------------------------------- 1 | import { format as d3format } from 'd3'; 2 | 3 | export default function drawOverallMark(chart) { 4 | //Clear overall marks. 5 | chart.svg.selectAll('.overall-mark').remove(); 6 | 7 | //For each mark draw an overall mark. 8 | chart.config.overall.forEach(d => { 9 | if (chart.config.y.order.indexOf(d.key) > -1) { 10 | const g = chart.svg.append('g').classed('overall-mark', true); 11 | const x = d[chart.config.x.column]; 12 | const y = d.key; 13 | 14 | //Draw vertical line representing the overall rate of the current categorical value. 15 | if (chart.y(y)) { 16 | const rateLine = g 17 | .append('line') 18 | .attr({ 19 | x1: chart.x(x), 20 | y1: chart.y(y), 21 | x2: chart.x(x), 22 | y2: chart.y(y) + chart.y.rangeBand() 23 | }) 24 | .style({ 25 | stroke: 'black', 26 | 'stroke-width': '2px', 27 | 'stroke-opacity': '1' 28 | }); 29 | rateLine 30 | .append('title') 31 | .text(`Overall rate: ${d3format(chart.config.x.format)(x)}`); 32 | } 33 | } 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/charts/horizontalBars/moveYaxis.js: -------------------------------------------------------------------------------- 1 | export default function moveYaxis(chart) { 2 | const ticks = chart.wrap.selectAll('g.y.axis g.tick'); 3 | ticks.select('text').remove(); 4 | ticks.append('title').text(d => d); 5 | ticks 6 | .append('text') 7 | .attr({ 8 | 'text-anchor': 'start', 9 | 'alignment-baseline': 'middle', 10 | dx: '2.5em', 11 | x: chart.plot_width 12 | }) 13 | .text(d => (d.length < 25 ? d : d.substring(0, 25) + '...')); 14 | } 15 | -------------------------------------------------------------------------------- /src/charts/horizontalBars/onInit.js: -------------------------------------------------------------------------------- 1 | export default function onInit() { 2 | //Add group labels. 3 | var chart = this; 4 | if (this.config.group_col) { 5 | const groupTitle = this.wrap 6 | .append('p') 7 | .attr('class', 'panel-label') 8 | .style('margin-left', chart.config.margin.left + 'px') 9 | .html( 10 | `${this.config.group_col}: ${ 11 | this.config.group_val 12 | } (n=${this.config.n})` 13 | ); 14 | this.wrap 15 | .node() 16 | .parentNode.insertBefore(groupTitle.node(), this.wrap.node()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/charts/horizontalBars/onResize.js: -------------------------------------------------------------------------------- 1 | import moveYaxis from './moveYaxis'; 2 | import drawOverallMark from './drawOverallMark'; 3 | import drawDifferences from './drawDifferences'; 4 | import highlightData from '../util/highlightData.js'; 5 | 6 | export default function onResize() { 7 | const context = this; 8 | 9 | moveYaxis(this); 10 | if (this.config.x.column === 'prop_n') { 11 | drawOverallMark(this); 12 | 13 | if (this.config.group_col) drawDifferences(this); 14 | } 15 | 16 | //Add event listener to marks to highlight data. 17 | highlightData(this); 18 | 19 | //hide legend 20 | this.legend.remove(); 21 | } 22 | -------------------------------------------------------------------------------- /src/charts/spark/makeHist.js: -------------------------------------------------------------------------------- 1 | import { select, scale, extent, layout, max } from 'd3'; 2 | 3 | export default function makeHist(this_, d) { 4 | var height = 15, 5 | width = 100; 6 | 7 | var svg = select(this_) 8 | .append('svg') 9 | .attr('height', height) 10 | .attr('width', width) 11 | .style('margin-right', '0.1em'); 12 | 13 | if (d.type == 'categorical') { 14 | var bins = d.statistics.values; 15 | bins.forEach(function(d) { 16 | d.title = d.key + ' - ' + d.n + ' (' + d.prop_n_text + ')'; 17 | d.color = '#999'; 18 | }); 19 | } else if (d.type == 'continuous') { 20 | var values = d.values.filter(f => !f.missing).map(function(m) { 21 | return +m.value; 22 | }); 23 | var x_linear = scale 24 | .linear() 25 | .domain(extent(values)) 26 | .range([0, width]); 27 | var bins = layout 28 | .histogram() 29 | .bins(x_linear.ticks(50))(values) 30 | .map(function(m, i) { 31 | m.key = '[' + m.x + '-' + (m.x + m.dx) + ')'; 32 | m.n = m.length; 33 | m.title = m.key + ' - ' + m.n; 34 | m.color = 'black'; 35 | return m; 36 | }); 37 | } 38 | 39 | // scales 40 | var x = scale 41 | .ordinal() 42 | .domain( 43 | bins.map(function(d) { 44 | return d.key; 45 | }) 46 | ) 47 | .rangeBands([0, width], 0.1, 0); 48 | 49 | var width = x.rangeBand(); 50 | 51 | var y = scale 52 | .linear() 53 | .domain([ 54 | 0, 55 | max(bins, function(d) { 56 | return d.n; 57 | }) 58 | ]) 59 | .range([height, 0]); 60 | 61 | var bar = svg 62 | .selectAll('.bar') 63 | .data(bins) 64 | .enter() 65 | .append('g') 66 | .attr('class', 'bar') 67 | .attr('transform', function(d) { 68 | return 'translate(' + x(d.key) + ',' + y(d.n) + ')'; 69 | }); 70 | 71 | bar 72 | .append('rect') 73 | .attr('x', 1) 74 | .attr('width', width) 75 | .attr('height', function(d) { 76 | return height - y(d.n); 77 | }) 78 | .attr('fill', d.type == 'categorical' ? '#999' : 'black') 79 | .append('title') 80 | .text(function(d) { 81 | return d.title; 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /src/charts/util/highlightData.js: -------------------------------------------------------------------------------- 1 | import { select as d3select } from 'd3'; 2 | import indicateLoading from '../../codebook/util/indicateLoading'; 3 | 4 | export default function highlightData(chart) { 5 | const codebook = d3select( 6 | chart.wrap.node().parentNode.parentNode.parentNode 7 | ).datum(), // codebook object is attached to .summaryTable element 8 | bars = chart.svg.selectAll('.bar-group'); 9 | 10 | bars.on('click', function(d) { 11 | indicateLoading(codebook, '.highlightCount', () => { 12 | const newIndexes = 13 | chart.config.chartType.indexOf('Bars') > -1 14 | ? d.values.raw[0].indexes 15 | : chart.config.chartType === 'histogramBoxPlot' 16 | ? d.values.raw.map(di => di.index) 17 | : []; 18 | const currentIndexes = codebook.data.highlighted.map( 19 | di => di['web-codebook-index'] 20 | ); 21 | const removeIndexes = currentIndexes.filter( 22 | di => newIndexes.indexOf(di) > -1 23 | ); 24 | 25 | codebook.data.highlighted = codebook.data.filtered.filter(di => { 26 | return removeIndexes.length 27 | ? currentIndexes.indexOf(di['web-codebook-index']) > -1 && 28 | removeIndexes.indexOf(di['web-codebook-index']) === -1 29 | : currentIndexes.indexOf(di['web-codebook-index']) > -1 || 30 | newIndexes.indexOf(di['web-codebook-index']) > -1; 31 | }); 32 | 33 | //Display highlighted data in listing & codebook. 34 | codebook.data.makeSummary(codebook); 35 | codebook.dataListing.init(codebook); 36 | codebook.summaryTable.draw(codebook); 37 | codebook.chartMaker.draw(codebook); 38 | codebook.title.updateCountSummary(codebook); 39 | }); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/charts/verticalBars/axisSort.js: -------------------------------------------------------------------------------- 1 | export default function axisSort(a, b, type) { 2 | var alpha = a.key < b.key ? -1 : 1; 3 | if (type == 'Alphabetical') { 4 | return alpha; 5 | } else if (type == 'Descending') { 6 | return a.prop_n > b.prop_n ? -2 : a.prop_n < b.prop_n ? 2 : alpha; 7 | } else if (type == 'Ascending') { 8 | return a.prop_n > b.prop_n ? 2 : a.prop_n < b.prop_n ? -2 : alpha; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/charts/verticalBars/makeTooltip.js: -------------------------------------------------------------------------------- 1 | import { format as d3format } from 'd3'; 2 | 3 | export default function makeTooltip(d, i, context) { 4 | const format = d3format(context.config.measureFormat); 5 | d.selector = `bar` + i; 6 | //Define tooltips. 7 | const tooltip = context.svg.append('g').attr('id', d.selector); 8 | const text = tooltip.append('text').attr({ 9 | id: 'text', 10 | x: context.x(d.values.x), 11 | y: context.plot_height, 12 | dy: '-.75em', 13 | 'font-size': '75%', 14 | 'font-weight': 'bold', 15 | fill: 'white' 16 | }); 17 | text 18 | .append('tspan') 19 | .attr({ 20 | x: context.x(d.values.x), 21 | dx: context.x(d.values.x) < context.plot_width / 2 ? '1em' : '-1em', 22 | 'text-anchor': 23 | context.x(d.values.x) < context.plot_width / 2 ? 'start' : 'end' 24 | }) 25 | .text(`${d.values.x}`); 26 | text 27 | .append('tspan') 28 | .attr({ 29 | x: context.x(d.values.x), 30 | dx: context.x(d.values.x) < context.plot_width / 2 ? '1em' : '-1em', 31 | dy: '-1.5em', 32 | 'text-anchor': 33 | context.x(d.values.x) < context.plot_width / 2 ? 'start' : 'end' 34 | }) 35 | .text('n=' + d.values.raw[0].n + ' (' + d3format('0.1%')(d.total) + ')'); 36 | const dimensions = text[0][0].getBBox(); 37 | tooltip.classed('svg-tooltip', true); //have to run after .getBBox() in FF/EI since this sets display:none 38 | 39 | const background = tooltip 40 | .append('rect') 41 | .attr({ 42 | id: 'background', 43 | x: dimensions.x - 5, 44 | y: dimensions.y - 2, 45 | width: dimensions.width + 10, 46 | height: dimensions.height + 4 47 | }) 48 | .style({ 49 | fill: 'black', 50 | stroke: 'white' 51 | }); 52 | tooltip[0][0].insertBefore(background[0][0], text[0][0]); 53 | } 54 | -------------------------------------------------------------------------------- /src/charts/verticalBars/moveYaxis.js: -------------------------------------------------------------------------------- 1 | import { format as d3format } from 'd3'; 2 | 3 | export default function moveYaxis(chart) { 4 | const ticks = chart.wrap.selectAll('g.y.axis g.tick'); 5 | ticks.select('text').remove(); 6 | ticks.append('title').text(d => d); 7 | ticks 8 | .append('text') 9 | .attr({ 10 | 'text-anchor': 'start', 11 | 'alignment-baseline': 'middle', 12 | dx: '.5em', 13 | x: chart.plot_width 14 | }) 15 | .text(d => d3format(chart.config.y.format)(d)); 16 | } 17 | -------------------------------------------------------------------------------- /src/charts/verticalBars/onInit.js: -------------------------------------------------------------------------------- 1 | export default function onInit() { 2 | //Add group labels. 3 | var chart = this; 4 | if (this.config.group_col) { 5 | const groupTitle = this.wrap 6 | .append('p') 7 | .attr('class', 'panel-label') 8 | .style('margin-left', chart.config.margin.left + 'px') 9 | .html( 10 | `${this.config.group_col}: ${ 11 | this.config.group_val 12 | } (n=${this.config.n})` 13 | ); 14 | this.wrap 15 | .node() 16 | .parentNode.insertBefore(groupTitle.node(), this.wrap.node()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/charts/verticalBars/onResize.js: -------------------------------------------------------------------------------- 1 | import moveYaxis from './moveYaxis'; 2 | import makeTooltip from './makeTooltip.js'; 3 | import { mouse as d3mouse } from 'd3'; 4 | import highlightData from '../util/highlightData.js'; 5 | 6 | export default function onResize() { 7 | const context = this; 8 | 9 | moveYaxis(this); 10 | //remove x-axis text 11 | var ticks = this.wrap.selectAll('g.x.axis g.tick'); 12 | ticks.select('text').remove(); 13 | this.svg.selectAll('g.bar-group').each(function(d, i) { 14 | makeTooltip(d, i, context); 15 | }); 16 | 17 | //Add modal to nearest mark. 18 | const bars = this.svg.selectAll('.bar-group:not(.sub)'); 19 | const tooltips = this.svg.selectAll('.svg-tooltip'); 20 | const statistics = this.svg.selectAll('.statistic'); 21 | 22 | this.svg 23 | .on('mousemove', function() { 24 | //Highlight closest bar. 25 | const mouse = d3mouse(this); 26 | const x = mouse[0]; 27 | const y = mouse[1]; 28 | let minimum; 29 | let bar = {}; 30 | bars.each(function(d, i) { 31 | d.distance = Math.abs(context.x(d.values.x) - x); 32 | if (i === 0 || d.distance < minimum) { 33 | minimum = d.distance; 34 | bar = d; 35 | } 36 | }); 37 | 38 | //In the instance of equally close bars, e.g. an unhighlighted and highlighted bar, choose one randomly. 39 | let closest = bars.filter(d => d.distance === minimum); 40 | if (closest.size() > 1) { 41 | let arbitrary; 42 | closest = closest.filter((d, i) => { 43 | if (i === 0) arbitrary = Math.round(Math.random()); 44 | return i === arbitrary; 45 | }); 46 | } 47 | bars 48 | .select('rect') 49 | .style('stroke-width', null) 50 | .style('stroke', null); 51 | closest = closest.select('rect'); 52 | 53 | //Activate tooltip. 54 | const d = closest.datum(); 55 | tooltips.classed('active', false); 56 | context.svg.select('#' + d.selector).classed('active', true); 57 | 58 | closest.style('stroke-width', '3px').style('stroke', 'black'); 59 | }) 60 | .on('mouseout', function() { 61 | context.svg.selectAll('g.svg-tooltip').classed('active', false); 62 | bars 63 | .select('rect') 64 | .style('stroke-width', null) 65 | .style('stroke', null); 66 | }); 67 | 68 | //Add event listener to marks to highlight data. 69 | highlightData(this); 70 | 71 | //hide legend 72 | this.legend.remove(); 73 | } 74 | -------------------------------------------------------------------------------- /src/codebook/chartMaker.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define chartmaker object 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { draw } from './chartMaker/draw'; 6 | import { init } from './chartMaker/init'; 7 | 8 | export const chartMaker = { 9 | draw: draw, 10 | init: init 11 | }; 12 | -------------------------------------------------------------------------------- /src/codebook/chartMaker/chartMakerSettings.js: -------------------------------------------------------------------------------- 1 | const chartMakerSettings = { 2 | width: 800, //changed to 300 for paneled charts 3 | aspect: 1.5, 4 | resizable: false, 5 | x: { 6 | column: null, 7 | type: null, 8 | label: null 9 | }, 10 | y: { 11 | column: null, 12 | type: null, 13 | label: null 14 | }, 15 | marks: [ 16 | { 17 | type: null, 18 | per: ['row_index'] 19 | } 20 | ], 21 | colors: ['#999', 'orange'], 22 | color_by: 'highlight' 23 | }; 24 | 25 | export default chartMakerSettings; 26 | -------------------------------------------------------------------------------- /src/codebook/chartMaker/columnSelect/init.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Initialize detail select 3 | \------------------------------------------------------------------------------------------------*/ 4 | import { select as d3select } from 'd3'; 5 | import { initAxisSelect } from './initAxisSelect.js'; 6 | 7 | export function init(codebook) { 8 | initAxisSelect(codebook); 9 | } 10 | -------------------------------------------------------------------------------- /src/codebook/chartMaker/columnSelect/initAxisSelect.js: -------------------------------------------------------------------------------- 1 | export function initAxisSelect(codebook) { 2 | //X & Y Variables 3 | var x_wrap = codebook.chartMaker.controlsWrap 4 | .append('span') 5 | .attr('class', 'control column-select x'); 6 | 7 | var y_wrap = codebook.chartMaker.controlsWrap 8 | .append('span') 9 | .attr('class', 'control column-select y'); 10 | 11 | x_wrap.append('small').html('x variable: '); 12 | y_wrap.append('small').html('y variable: '); 13 | 14 | var x_select = x_wrap.append('select'); 15 | var y_select = y_wrap.append('select'); 16 | 17 | var axisOptions = codebook.data.summary 18 | .filter( 19 | f => 20 | f.type == 'continuous' || 21 | codebook.config.groups.map(m => m.value_col).indexOf(f.value_col) >= 0 22 | ) 23 | .filter(f => f.label != 'web-codebook-index'); 24 | 25 | var x_items = x_select 26 | .selectAll('option') 27 | .data(axisOptions) 28 | .enter() 29 | .append('option') 30 | .property('selected', function(d, i) { 31 | return i == 0; 32 | }) 33 | .html(d => d.label); 34 | 35 | var y_items = y_select 36 | .selectAll('option') 37 | .data(axisOptions) 38 | .enter() 39 | .append('option') 40 | .property('selected', function(d, i) { 41 | return i == 1; 42 | }) 43 | .html(d => d.label); 44 | 45 | //Handlers for label events 46 | x_select.on('change', function() { 47 | codebook.chartMaker.draw(codebook); 48 | }); 49 | 50 | y_select.on('change', function() { 51 | codebook.chartMaker.draw(codebook); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/codebook/chartMaker/draw.js: -------------------------------------------------------------------------------- 1 | import { createChart } from 'webcharts'; 2 | import { multiply } from 'webcharts'; 3 | import indicateLoading from '../util/indicateLoading'; 4 | import clone from '../../util/clone'; 5 | import chartMakerSettings from './chartMakerSettings.js'; 6 | import makeSettings from './makeSettings.js'; 7 | 8 | export function draw(codebook) { 9 | indicateLoading(codebook, '.web-codebook .chartMaker'); 10 | const chartMaker = codebook.chartMaker; 11 | 12 | //clear current chart 13 | chartMaker.chartWrap.selectAll('*').remove(); 14 | chartMaker.wrap.selectAll('.status.error').remove(); 15 | 16 | //get selected variable objects 17 | var x_var = chartMaker.controlsWrap 18 | .select('.column-select.x select') 19 | .property('value'); 20 | var x_obj = codebook.data.summary.filter(f => f.label == x_var)[0]; 21 | 22 | var y_var = chartMaker.controlsWrap 23 | .select('.column-select.y select') 24 | .property('value'); 25 | var y_obj = codebook.data.summary.filter(f => f.label == y_var)[0]; 26 | 27 | //get settings and data for the chart 28 | if (x_obj == undefined || y_obj == undefined) { 29 | chartMaker.wrap 30 | .append('div') 31 | .attr('class', 'status error') 32 | .text( 33 | 'No continuous and/or group variables available to plot. Visit the settings tabs to update variable settings.' 34 | ); 35 | } else { 36 | chartMaker.chartSettings = makeSettings(chartMakerSettings, x_obj, y_obj); 37 | chartMaker.chartSettings.width = codebook.config.group ? 320 : 600; 38 | chartMaker.chartData = clone(codebook.data.filtered); 39 | 40 | //flag highlighted rows 41 | var highlightedRows = codebook.data.highlighted.map( 42 | m => m['web-codebook-index'] 43 | ); 44 | chartMaker.chartData.forEach(function(d) { 45 | d.highlight = highlightedRows.indexOf(d['web-codebook-index']) > -1; 46 | }); 47 | 48 | //Define chart. 49 | chartMaker.chart = createChart( 50 | codebook.wrap.select('.chartMaker.section .cm-chart').node(), 51 | chartMaker.chartSettings 52 | ); 53 | 54 | //remove legend unless it's a bar chart 55 | chartMaker.chart.on('resize', function() { 56 | if (this.config.legend.label == 'highlight') { 57 | this.legend.remove(); 58 | } 59 | }); 60 | 61 | if (codebook.config.group) { 62 | chartMaker.chart.on('draw', function() { 63 | var level = this.wrap.select('.wc-chart-title').text(); 64 | this.wrap 65 | .select('.wc-chart-title') 66 | .text(codebook.config.group + ': ' + level); 67 | }); 68 | multiply(chartMaker.chart, chartMaker.chartData, codebook.config.group); 69 | } else { 70 | chartMaker.chart.init(chartMaker.chartData); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/codebook/chartMaker/init.js: -------------------------------------------------------------------------------- 1 | import { init as initControls } from './columnSelect/init.js'; 2 | 3 | export function init(codebook) { 4 | const chartMaker = codebook.chartMaker; 5 | chartMaker.codebook = codebook; 6 | chartMaker.config = codebook.config; 7 | 8 | //layout 9 | chartMaker.wrap.selectAll('*').remove(); 10 | chartMaker.controlsWrap = chartMaker.wrap 11 | .append('div') 12 | .attr('class', 'cm-controls'); 13 | chartMaker.chartWrap = chartMaker.wrap 14 | .append('div') 15 | .attr('class', 'cm-chart'); 16 | 17 | if (codebook.data.summary.length > 2) { 18 | initControls(codebook); //make controls 19 | chartMaker.draw(codebook); //draw the initial codebook 20 | } else { 21 | chartMaker.wrap 22 | .append('div') 23 | .attr('class', 'status') 24 | .text('Two or more variables required to use Charts module.'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/codebook/chartMaker/makeSettings.js: -------------------------------------------------------------------------------- 1 | // Makes a valid settings object for the current selections. 2 | // settings is the settings object that needs updated 3 | // xvar and yvar are data objects created by codebook/data/makeSummary.js 4 | 5 | export default function makeSettings(settings, xvar, yvar) { 6 | //set x config 7 | settings.x = { 8 | column: xvar.value_col, 9 | label: xvar.label, 10 | type: xvar.type == 'categorical' ? 'ordinal' : 'linear' 11 | }; 12 | 13 | //set y config 14 | settings.y = { 15 | column: yvar.value_col, 16 | label: yvar.label, 17 | type: yvar.type == 'categorical' ? 'ordinal' : 'linear' 18 | }; 19 | 20 | // set mark and color 21 | if ((settings.x.type == 'linear') & (settings.y.type == 'linear')) { 22 | //mark types: x = linear vs. y = linear 23 | settings.marks = [ 24 | { 25 | type: 'circle', 26 | per: ['web-codebook-index'] 27 | } 28 | ]; 29 | settings.legend = null; 30 | settings.color_by = 'highlight'; 31 | settings.colors = ['#999', 'orange']; 32 | } else if ((settings.x.type == 'linear') & (settings.y.type == 'ordinal')) { 33 | //mark types: x = linear vs. y = ordinal 34 | settings.marks = [ 35 | { 36 | type: 'circle', 37 | per: ['web-codebook-index'] 38 | }, 39 | { 40 | type: 'text', 41 | text: '|', 42 | per: [yvar.value_col], 43 | summarizeX: 'mean', 44 | attributes: { 'text-anchor': 'middle', 'alignment-baseline': 'middle' } 45 | } 46 | ]; 47 | settings.legend = null; 48 | settings.color_by = 'highlight'; 49 | settings.colors = ['#999', 'orange']; 50 | } else if ((settings.x.type == 'ordinal') & (settings.y.type == 'linear')) { 51 | //mark types: x = ordinal vs. y = linear 52 | settings.marks = [ 53 | { 54 | type: 'circle', 55 | per: ['web-codebook-index'] 56 | }, 57 | { 58 | type: 'text', 59 | text: '---', 60 | per: [xvar.value_col], 61 | summarizeY: 'mean', 62 | attributes: { 'text-anchor': 'middle', 'alignment-baseline': 'middle' } 63 | } 64 | ]; 65 | settings.legend = null; 66 | settings.color_by = 'highlight'; 67 | settings.colors = ['#999', 'orange']; 68 | } else if ((settings.x.type == 'ordinal') & (settings.y.type == 'ordinal')) { 69 | //mark types: x = ordinal vs. y = ordinal 70 | 71 | settings.y = { 72 | column: '', 73 | type: 'linear', 74 | label: 'Number of observations', 75 | domain: [0, null] 76 | }; 77 | settings.marks = [ 78 | { 79 | type: 'bar', 80 | arrange: 'stacked', 81 | split: yvar.value_col, 82 | per: [xvar.value_col], 83 | summarizeY: 'count' 84 | } 85 | ]; 86 | settings.legend = { label: yvar.label }; 87 | settings.color_by = yvar.value_col; 88 | settings.colors = [ 89 | '#e41a1c', 90 | '#377eb8', 91 | '#4daf4a', 92 | '#984ea3', 93 | '#ff7f00', 94 | '#ffff33', 95 | '#a65628', 96 | '#f781bf', 97 | '#999999' 98 | ]; 99 | } 100 | return settings; 101 | } 102 | -------------------------------------------------------------------------------- /src/codebook/controls.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define controls object. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { init } from './controls/init'; 6 | import { filters } from './controls/filters'; 7 | import { groups } from './controls/groups'; 8 | import { controlToggle } from './controls/controlToggle'; 9 | 10 | export const controls = { 11 | init: init, 12 | filters: filters, 13 | groups: groups, 14 | controlToggle: controlToggle 15 | }; 16 | -------------------------------------------------------------------------------- /src/codebook/controls/controlToggle.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define chart toggle object. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { init } from './controlToggle/init'; 6 | import { set } from './controlToggle/set'; 7 | 8 | export const controlToggle = { 9 | init: init, 10 | set: set 11 | }; 12 | -------------------------------------------------------------------------------- /src/codebook/controls/controlToggle/init.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Initialize controls container hide/show toggle. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { select as d3select } from 'd3'; 6 | 7 | export function init(codebook) { 8 | //render the control 9 | var controlToggle = codebook.controls.wrap 10 | .append('button') 11 | .attr('class', 'control-toggle'); 12 | 13 | //set the initial 14 | codebook.controls.controlToggle.set(codebook); 15 | 16 | controlToggle.on('click', function() { 17 | codebook.config.controlVisibility = 18 | d3select(this).text() == 'Hide' 19 | ? 'minimized' //click "-" to minimize controls 20 | : 'visible'; // click "+" to show controls 21 | 22 | codebook.controls.controlToggle.set(codebook); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/codebook/controls/controlToggle/set.js: -------------------------------------------------------------------------------- 1 | export function set(codebook) { 2 | //update toggle text 3 | codebook.controls.wrap 4 | .select('button.control-toggle') 5 | .text(codebook.config.controlVisibility == 'visible' ? 'Hide' : 'Show'); 6 | codebook.controls.wrap.attr( 7 | 'class', 8 | 'controls section ' + codebook.config.controlVisibility 9 | ); 10 | 11 | //hide the controls if controlVisibility isn't "visible" ... 12 | codebook.controls.wrap 13 | .selectAll('div') 14 | .classed('hidden', !(codebook.config.controlVisibility == 'visible')); 15 | 16 | // but show the title and the toggle ... 17 | codebook.controls.wrap.select('div.controls-title').classed('hidden', false); 18 | codebook.controls.wrap 19 | .select('button.control-toggle') 20 | .classed('hidden', false); 21 | 22 | // unless control visibility is hidden, in which case just hide it all 23 | codebook.controls.wrap.classed( 24 | 'hidden', 25 | codebook.config.controlVisibility == 'hidden' || 26 | codebook.config.controlVisibility == 'disabled' 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/codebook/controls/filters.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define filter controls object. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { init } from './filters/init'; 6 | import { update } from './filters/update'; 7 | 8 | export const filters = { 9 | init: init, 10 | update: update 11 | }; 12 | -------------------------------------------------------------------------------- /src/codebook/controls/filters/init.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Initialize filters. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { nest as d3nest, select as d3select } from 'd3'; 6 | import { update } from './update'; 7 | 8 | //export function init(selector, data, vars, settings) { 9 | export function init(codebook) { 10 | //initialize the wrapper 11 | const selector = codebook.controls.wrap 12 | .append('div') 13 | .attr('class', 'custom-filters'), 14 | filterList = selector.append('ul').attr('class', 'filter-list'); 15 | 16 | update(codebook); 17 | } 18 | -------------------------------------------------------------------------------- /src/codebook/controls/filters/update.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Update filters. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { nest as d3nest, select as d3select } from 'd3'; 6 | import indicateLoading from '../../util/indicateLoading'; 7 | 8 | export function update(codebook) { 9 | const selector = codebook.controls.wrap.select('div.custom-filters'), 10 | filterList = selector.select('ul.filter-list'); 11 | 12 | //add a list of values to each filter object 13 | codebook.config.filters.forEach(function(e) { 14 | if (!e.hasOwnProperty('values')) 15 | e.values = d3nest() 16 | .key(function(d) { 17 | return d[e.value_col]; 18 | }) 19 | .entries(codebook.data.raw) 20 | .map(function(d) { 21 | var obj = { value: d.key, selected: true }; 22 | obj.label = /^\s*$/.test(d.key) ? '[No value provided]' : d.key; 23 | return obj; 24 | }); 25 | e.label = codebook.data.summary.filter( 26 | d => d.value_col === e.value_col 27 | )[0].label; 28 | }); 29 | 30 | //Add filter controls. 31 | var allFilterItem = filterList 32 | .selectAll('li') 33 | .data(codebook.config.filters, d => d.value_col); 34 | var columns = Object.keys(codebook.data.raw[0]); 35 | allFilterItem.exit().remove(); 36 | var filterItem = allFilterItem 37 | .enter() 38 | .append('li') 39 | .attr('class', function(d) { 40 | return 'custom-' + d.value_col + ' filterCustom'; 41 | }); 42 | allFilterItem.classed( 43 | 'hidden', 44 | d => codebook.config.hiddenVariables.indexOf(d.value_col) > -1 45 | ); 46 | allFilterItem.sort((a, b) => { 47 | const aSort = columns.indexOf(a.value_col), 48 | bSort = columns.indexOf(b.value_col); 49 | return aSort - bSort; 50 | }); 51 | 52 | var filterLabel = filterItem.append('span').attr('class', 'filterLabel'); 53 | 54 | filterLabel 55 | .append('span') 56 | .classed('filter-variable', true) 57 | .html(d => d.value_col); 58 | filterLabel 59 | .append('span') 60 | .classed('filter-label', true) 61 | .html(d => (d.value_col !== d.label ? d.label : '')); 62 | 63 | var filterCustom = filterItem.append('select').attr('multiple', true); 64 | 65 | //Add data-driven filter options. 66 | var filterItems = filterCustom 67 | .selectAll('option') 68 | .data(function(d) { 69 | return d.values; 70 | }) 71 | .enter() 72 | .append('option') 73 | .html(function(d) { 74 | return d.label; 75 | }) 76 | .attr('value', function(d) { 77 | return d.value; 78 | }) 79 | .attr('selected', d => (d.selected ? 'selected' : null)); 80 | 81 | //Initialize event listeners 82 | var filters = codebook.controls.wrap 83 | .selectAll('.filterCustom select') 84 | .on('change', function(d) { 85 | indicateLoading(codebook, '#loading-indicator', () => { 86 | // flag the selected options in the config 87 | var options = d3select(this).selectAll('option'); 88 | options.each(function(option_d) { 89 | option_d.selected = d3select(this).property('selected'); 90 | }); 91 | codebook.config.filters.filter( 92 | filter => filter.value_col === d.value_col 93 | )[0].values = options.data(); 94 | 95 | //update the codebook 96 | codebook.data.filtered = codebook.data.makeFiltered( 97 | codebook.data.raw, 98 | codebook.config.filters 99 | ); 100 | 101 | //clear highlights 102 | codebook.data.highlighted = []; 103 | codebook.data.makeSummary(codebook); 104 | codebook.title.updateCountSummary(codebook); 105 | codebook.summaryTable.draw(codebook); 106 | codebook.chartMaker.draw(codebook); 107 | codebook.dataListing.init(codebook); 108 | }); 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /src/codebook/controls/groups.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define filter controls object. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { init } from './groups/init'; 6 | import { update } from './groups/update'; 7 | 8 | export const groups = { 9 | init: init, 10 | update: update 11 | }; 12 | -------------------------------------------------------------------------------- /src/codebook/controls/groups/init.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Initialize group control. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { merge as d3merge } from 'd3'; 6 | import { update } from './update'; 7 | 8 | export function init(codebook) { 9 | var selector = codebook.controls.wrap 10 | .append('div') 11 | .attr('class', 'group-select'); 12 | selector.append('span').text('Group by'); 13 | var groupSelect = selector.append('select'); 14 | update(codebook); 15 | } 16 | -------------------------------------------------------------------------------- /src/codebook/controls/groups/update.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Update group control. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { merge as d3merge } from 'd3'; 6 | import { select as d3select } from 'd3'; 7 | import indicateLoading from '../../util/indicateLoading'; 8 | 9 | export function update(codebook) { 10 | var groupControl = codebook.controls.wrap.select('div.group-select'), 11 | groupSelect = groupControl.select('select'), 12 | columns = Object.keys(codebook.data.raw[0]), 13 | groupLevels = d3merge([ 14 | [{ value_col: 'None', label: 'None' }], 15 | codebook.config.groups.map(m => { 16 | return { 17 | value_col: m.value_col, 18 | label: codebook.data.summary.filter( 19 | variable => variable.value_col === m.value_col 20 | )[0].label 21 | }; 22 | }) 23 | ]), 24 | groupOptions = groupSelect 25 | .selectAll('option') 26 | .data(groupLevels, d => d.value_col); 27 | groupOptions 28 | .enter() 29 | .append('option') 30 | .property( 31 | 'label', 32 | d => 33 | d.value_col !== d.label ? `${d.value_col} (${d.label})` : d.value_col 34 | ) 35 | .text(d => d.value_col); 36 | groupOptions.exit().remove(); 37 | var visibleOptionCount = 0; 38 | groupOptions.classed('hidden', function(d) { 39 | const hidden = codebook.config.hiddenVariables.indexOf(d.value_col) > -1; 40 | if (!hidden) visibleOptionCount = visibleOptionCount + 1; 41 | return hidden; 42 | }); 43 | 44 | groupOptions.sort((a, b) => columns.indexOf(a) - columns.indexOf(b)); 45 | groupSelect.on('change', function() { 46 | indicateLoading(codebook, '#loading-indicator', () => { 47 | if (this.value !== 'None') codebook.config.group = this.value; 48 | else delete codebook.config.group; 49 | 50 | codebook.data.highlighted = []; 51 | codebook.data.makeSummary(codebook); 52 | codebook.summaryTable.draw(codebook); 53 | codebook.chartMaker.draw(codebook); 54 | codebook.title.updateCountSummary(codebook); 55 | }); 56 | }); 57 | 58 | //Hide the group select if only the "None" option is visible; 59 | groupControl.style('display', visibleOptionCount <= 1 ? 'none' : null); 60 | } 61 | -------------------------------------------------------------------------------- /src/codebook/controls/init.js: -------------------------------------------------------------------------------- 1 | import indicateLoading from '../util/indicateLoading'; 2 | 3 | export function init(codebook) { 4 | indicateLoading(codebook, '.web-codebook .controls .control-toggle'); 5 | 6 | codebook.controls.wrap.attr('onsubmit', 'return false;'); 7 | codebook.controls.wrap.selectAll('*:not(#loading-indicator)').remove(); //Clear controls. 8 | 9 | //Draw title 10 | codebook.controls.title = codebook.controls.wrap 11 | .append('div') 12 | .attr('class', 'controls-title') 13 | .text('Controls'); 14 | codebook.controls.summaryWrap = codebook.controls.title.append('span'); 15 | codebook.controls.rowCount = codebook.controls.summaryWrap 16 | .append('span') 17 | .attr('class', 'rowCount'); 18 | codebook.controls.highlightCount = codebook.controls.summaryWrap 19 | .append('span') 20 | .attr('class', 'highlightCount'); 21 | 22 | //Draw controls. 23 | codebook.controls.groups.init(codebook); 24 | codebook.controls.filters.init(codebook); 25 | codebook.controls.controlToggle.init(codebook); 26 | codebook.title.updateCountSummary(codebook); 27 | 28 | //Hide group-by options corresponding to variables specified in settings.hiddenVariables. 29 | codebook.controls.wrap 30 | .selectAll('.group-select option') 31 | .classed('hidden', d => codebook.config.hiddenVariables.indexOf(d) > -1); 32 | 33 | //Hide filters corresponding to variables specified in settings.hiddenVariables. 34 | codebook.controls.wrap 35 | .selectAll('.filter-list li.filterCustom') 36 | .classed( 37 | 'hidden', 38 | d => codebook.config.hiddenVariables.indexOf(d.value_col) > -1 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/codebook/data.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define data object. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { makeFiltered } from './data/makeFiltered'; 6 | import { makeSummary } from './data/makeSummary'; 7 | 8 | export const data = { 9 | makeFiltered: makeFiltered, 10 | makeSummary: makeSummary 11 | }; 12 | -------------------------------------------------------------------------------- /src/codebook/data/makeFiltered.js: -------------------------------------------------------------------------------- 1 | export function makeFiltered(data, filters) { 2 | var filtered = data; 3 | filters.forEach(function(filter_d) { 4 | //remove the filtered values from the data based on the filters 5 | filtered = filtered.filter(function(rowData) { 6 | var currentValues = filter_d.values 7 | .filter(f => f.selected) 8 | .map(m => m.value); 9 | return currentValues.indexOf('' + rowData[filter_d.value_col]) > -1; 10 | }); 11 | }); 12 | return filtered; 13 | } 14 | -------------------------------------------------------------------------------- /src/codebook/data/makeSummary.js: -------------------------------------------------------------------------------- 1 | import { nest, set as d3set } from 'd3'; 2 | import summarize from './summarize/index'; 3 | 4 | export function makeSummary(codebook) { 5 | const config = codebook.config; 6 | var data = codebook.data.filtered; 7 | var group = codebook.config.group; 8 | 9 | if (codebook.data.filtered.length > 0) { 10 | const variables = Object.keys(data[0]).map(function(variable) { 11 | //change from string to object 12 | var varObj = { value_col: variable }; 13 | 14 | //get a list of raw values 15 | varObj.values = data.map(d => { 16 | var current = { 17 | index: d['web-codebook-index'], 18 | value: d[variable], 19 | highlighted: codebook.data.highlighted.indexOf(d) > -1, 20 | missingWhiteSpace: config.whiteSpaceAsMissing 21 | ? /^\s*$/.test(d[variable]) 22 | : false, 23 | missingValue: config.missingValues.indexOf(d[variable]) > -1 24 | }; 25 | current.missing = current.missingWhiteSpace || current.missingValue; 26 | 27 | return current; 28 | }); 29 | 30 | //get hidden status 31 | varObj.hidden = codebook.config.hiddenVariables.indexOf(variable) > -1; 32 | varObj.chartVisibility = codebook.config.chartVisibility; 33 | 34 | //get variable label 35 | varObj.label = 36 | codebook.config.variableLabels 37 | .map(variableLabel => variableLabel.value_col) 38 | .indexOf(variable) > -1 39 | ? codebook.config.variableLabels.filter( 40 | variableLabel => variableLabel.value_col === variable 41 | )[0].label 42 | : variable; 43 | 44 | //Determine Type 45 | varObj.type = 46 | codebook.config.variableTypes 47 | .map(variableType => variableType.value_col) 48 | .indexOf(variable) > -1 49 | ? codebook.config.variableTypes.filter( 50 | variableLabel => variableLabel.value_col === variable 51 | )[0].type 52 | : summarize.determineType(varObj.values, codebook.config.levelSplit); 53 | 54 | // update missingness for non-numeric values in continuous columns 55 | if (varObj.type == 'continuous') { 56 | varObj.values.forEach(function(d, i) { 57 | d.numeric = !isNaN(d.value) && !isNaN(parseFloat(d.value)); 58 | d.missing = d.missing || !d.numeric; 59 | }); 60 | } 61 | 62 | //create a list of missing values 63 | const missings = varObj.values.filter(f => f.missing).map(m => m.value); 64 | if (missings.length) { 65 | varObj.missingList = nest() 66 | .key(d => d) 67 | .rollup(d => d.length) 68 | .entries(missings) 69 | .sort((a, b) => b.values - a.values); 70 | 71 | varObj.missingSummary = varObj.missingList 72 | .map(m => '"' + m.key + '" (n=' + m.values + ')') 73 | .join('\n'); 74 | } else { 75 | varObj.missingList = []; 76 | } 77 | 78 | // Add metadata Object 79 | varObj.meta = []; 80 | var metaMatch = codebook.config.meta.filter(f => f.value_col == variable); 81 | if (metaMatch.length == 1) { 82 | var metaKeys = Object.keys(metaMatch[0]).filter( 83 | f => ['value_col', 'label'].indexOf(f) === -1 84 | ); 85 | metaKeys.forEach(function(m) { 86 | varObj.meta.push({ key: m, value: metaMatch[0][m] }); 87 | }); 88 | } 89 | 90 | //calculate variable statistics (including for highlights - if any) 91 | var sub = 92 | codebook.data.highlighted.length > 0 93 | ? function(d) { 94 | return d.highlighted; 95 | } 96 | : null; 97 | varObj.statistics = 98 | varObj.type === 'continuous' 99 | ? summarize.continuous(varObj.values, sub) 100 | : summarize.categorical(varObj.values, sub); 101 | 102 | //get chart type 103 | varObj.chartType = 'none'; 104 | if (varObj.type == 'continuous') { 105 | varObj.chartType = 'histogramBoxPlot'; 106 | } else if (varObj.type == 'categorical') { 107 | if (varObj.statistics.values.length > codebook.config.maxLevels) { 108 | varObj.chartType = 'character'; 109 | varObj.summaryText = 110 | 'Character variable with ' + 111 | varObj.statistics.values.length + 112 | ' unique levels.
' + 113 | "Click here to treat this variable as categorical and draw a histogram with " + 114 | varObj.statistics.values.length + 115 | ' levels. Note that this may slow down or crash your browser.'; 116 | } else if ( 117 | varObj.statistics.values.length > codebook.config.levelSplit 118 | ) { 119 | varObj.chartType = 'verticalBars'; 120 | } else if ( 121 | varObj.statistics.values.length <= codebook.config.levelSplit 122 | ) { 123 | varObj.chartType = 'horizontalBars'; 124 | } 125 | } 126 | 127 | //Handle groups. 128 | if (group) { 129 | varObj.group = group; 130 | varObj.groupLabel = 131 | codebook.config.variableLabels 132 | .map(variableLabel => variableLabel.value_col) 133 | .indexOf(group) > -1 134 | ? codebook.config.variableLabels.filter( 135 | variableLabel => variableLabel.value_col === group 136 | )[0].label 137 | : group; 138 | varObj.groups = d3set(data.map(d => d[group])) 139 | .values() 140 | .map(g => { 141 | return { group: g }; 142 | }); 143 | 144 | varObj.groups.forEach(g => { 145 | //Define variable metadata and generate data array. 146 | g.value_col = variable; 147 | g.values = data.filter(d => d[group] === g.group).map(d => { 148 | return { 149 | index: d['web-codebook-index'], 150 | value: d[variable], 151 | highlighted: codebook.data.highlighted.indexOf(d) > -1 152 | }; 153 | }); 154 | g.type = varObj.type; 155 | 156 | //Calculate statistics. 157 | if (varObj.type === 'categorical') 158 | g.statistics = summarize.categorical(g.values, sub); 159 | else g.statistics = summarize.continuous(g.values, sub); 160 | }); 161 | } 162 | return varObj; 163 | }); 164 | 165 | codebook.data.summary = variables; 166 | //get bin counts 167 | codebook.util.getBinCounts(codebook); 168 | } else { 169 | codebook.data.summary = []; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/codebook/data/summarize/categorical.js: -------------------------------------------------------------------------------- 1 | import { nest as d3nest, format as d3format, set as d3set } from 'd3'; 2 | 3 | export default function categorical(vector, sub) { 4 | const statistics = {}; 5 | statistics.N = vector.length; 6 | const nonMissing = vector.filter(f => !f.missing); 7 | statistics.n = nonMissing.length; 8 | statistics.nMissing = vector.length - statistics.n; 9 | statistics.percentMissing = statistics.nMissing / statistics.N; 10 | statistics.missingSummary = 11 | statistics.nMissing + 12 | '/' + 13 | statistics.N + 14 | ' (' + 15 | d3format('0.1%')(statistics.percentMissing) + 16 | ')'; 17 | statistics.values = d3nest() 18 | .key(d => d.value) 19 | .rollup(function(d) { 20 | var stats = { 21 | n: d.length, 22 | prop_N: d.length / statistics.N, 23 | prop_n: d.length / statistics.n, 24 | prop_N_text: d3format('0.1%')(d.length / statistics.N), 25 | prop_n_text: d3format('0.1%')(d.length / statistics.n), 26 | indexes: d.map(di => di.index) 27 | }; 28 | return stats; 29 | }) 30 | .entries(nonMissing); 31 | 32 | statistics.Unique = d3set(nonMissing.map(d => d.value)).values().length; 33 | 34 | statistics.values.forEach(value => { 35 | for (var statistic in value.values) { 36 | value[statistic] = value.values[statistic]; 37 | } 38 | delete value.values; 39 | }); 40 | 41 | if (sub) { 42 | statistics.highlightValues = d3nest() 43 | .key(d => d.value) 44 | .rollup(function(d) { 45 | var stats = { 46 | n: d.length, 47 | prop_N: d.length / statistics.N, 48 | prop_n: d.length / statistics.n, 49 | prop_N_text: d3format('0.1%')(d.length / statistics.N), 50 | prop_n_text: d3format('0.1%')(d.length / statistics.n), 51 | indexes: d.map(di => di.index) 52 | }; 53 | return stats; 54 | }) 55 | .entries(nonMissing.filter(sub)); 56 | 57 | statistics.highlightValues.forEach(value => { 58 | for (var statistic in value.values) { 59 | value[statistic] = value.values[statistic]; 60 | } 61 | delete value.values; 62 | }); 63 | } 64 | 65 | return statistics; 66 | } 67 | -------------------------------------------------------------------------------- /src/codebook/data/summarize/continuous.js: -------------------------------------------------------------------------------- 1 | import { 2 | format as d3format, 3 | mean as d3mean, 4 | deviation as d3deviation, 5 | quantile as d3quantile 6 | } from 'd3'; 7 | 8 | export default function continuous(vector, sub) { 9 | const statistics = {}; 10 | statistics.N = vector.length; 11 | const nonMissing = vector 12 | .filter(d => !d.missing) 13 | .map(d => +d.value) 14 | .sort((a, b) => a - b); 15 | statistics.n = nonMissing.length; 16 | statistics.nMissing = vector.length - statistics.n; 17 | statistics.percentMissing = statistics.nMissing / statistics.N; 18 | statistics.missingSummary = 19 | statistics.nMissing + 20 | '/' + 21 | statistics.N + 22 | ' (' + 23 | d3format('0.1%')(statistics.percentMissing) + 24 | ')'; 25 | statistics.mean = d3format('0.2f')(d3mean(nonMissing)); 26 | statistics.SD = d3format('0.2f')(d3deviation(nonMissing)); 27 | const quantiles = [ 28 | ['min', 0], 29 | ['5th percentile', 0.05], 30 | ['1st quartile', 0.25], 31 | ['median', 0.5], 32 | ['3rd quartile', 0.75], 33 | ['95th percentile', 0.95], 34 | ['max', 1] 35 | ]; 36 | quantiles.forEach(quantile => { 37 | let statistic = quantile[0]; 38 | statistics[statistic] = d3format('0.1f')( 39 | d3quantile(nonMissing, quantile[1]) 40 | ); 41 | }); 42 | 43 | if (sub) { 44 | var sub_vector = vector 45 | .filter(sub) 46 | .filter(d => !isNaN(+d.value) && !/^\s*$/.test(d.value)) 47 | .map(d => +d.value) 48 | .sort((a, b) => a - b); 49 | statistics.mean_sub = d3format('0.2f')(d3mean(sub_vector)); 50 | statistics.SD_sub = d3format('0.2f')(d3deviation(sub_vector)); 51 | quantiles.forEach(quantile => { 52 | let statistic = quantile[0]; 53 | statistics[statistic + '_sub'] = d3format('0.1f')( 54 | d3quantile(sub_vector, quantile[1]) 55 | ); 56 | }); 57 | } 58 | 59 | return statistics; 60 | } 61 | -------------------------------------------------------------------------------- /src/codebook/data/summarize/determineType.js: -------------------------------------------------------------------------------- 1 | import { set as d3set } from 'd3'; 2 | 3 | export default function determineType(vector, levelSplit) { 4 | const nonMissingValues = vector.filter(f => !f.missing); 5 | const numericValues = nonMissingValues.filter(d => !isNaN(+d.value)); 6 | const distinctValues = d3set(numericValues.map(d => d.value)).values(); 7 | 8 | return nonMissingValues.length === numericValues.length && 9 | distinctValues.length > levelSplit 10 | ? 'continuous' 11 | : 'categorical'; 12 | } 13 | -------------------------------------------------------------------------------- /src/codebook/data/summarize/index.js: -------------------------------------------------------------------------------- 1 | import determineType from './determineType'; 2 | import categorical from './categorical'; 3 | import continuous from './continuous'; 4 | 5 | export default { 6 | determineType: determineType, 7 | categorical: categorical, 8 | continuous: continuous 9 | }; 10 | -------------------------------------------------------------------------------- /src/codebook/dataListing.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define dataListing object (the meat and potatoes). 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { init } from './dataListing/init'; 6 | 7 | export const dataListing = { init: init }; 8 | -------------------------------------------------------------------------------- /src/codebook/dataListing/init.js: -------------------------------------------------------------------------------- 1 | import onDraw from './onDraw'; 2 | import { createTable } from 'webcharts'; 3 | import indicateLoading from '../util/indicateLoading'; 4 | 5 | export function init(codebook) { 6 | //indicateLoading(codebook, '.web-codebook .dataListing .wc-chart'); 7 | 8 | const dataListing = codebook.dataListing; 9 | dataListing.codebook = codebook; 10 | dataListing.config = codebook.config; 11 | dataListing.wrap.selectAll('*').remove(); 12 | 13 | //Define table. 14 | dataListing.table = createTable( 15 | codebook.wrap.select('.dataListing').node(), 16 | {} 17 | ); 18 | 19 | //Define callback. 20 | onDraw(dataListing); 21 | 22 | //Initialize table. 23 | dataListing.super_raw_data = codebook.data.filtered; 24 | dataListing.sorted_raw_data = codebook.data.filtered.sort(function(a, b) { 25 | var a_highlight = codebook.data.highlighted.indexOf(a) > -1; 26 | var b_highlight = codebook.data.highlighted.indexOf(b) > -1; 27 | if (a_highlight == b_highlight) { 28 | return 0; 29 | } else if (a_highlight) { 30 | return -1; 31 | } else if (b_highlight) { 32 | return 1; 33 | } 34 | }); 35 | 36 | dataListing.table.init(dataListing.sorted_raw_data); 37 | } 38 | -------------------------------------------------------------------------------- /src/codebook/dataListing/onDraw.js: -------------------------------------------------------------------------------- 1 | import { select as d3select } from 'd3'; 2 | 3 | export default function onDraw(dataListing) { 4 | dataListing.table.on('draw', function() { 5 | //Attach variable name rather than variable label to header to be able to apply settings.hiddenVariables to column headers. 6 | this.table.selectAll('th').attr('title', d => { 7 | const label = dataListing.config.variableLabels.filter( 8 | di => di.value_col === d 9 | )[0]; 10 | return label ? label.label : null; 11 | }); 12 | 13 | //Hide data listing columns corresponding to variables specified in settings.hiddenVariables. 14 | this.table 15 | .selectAll('th,td') 16 | .classed( 17 | 'hidden', 18 | d => dataListing.config.hiddenVariables.indexOf(d.col ? d.col : d) > -1 19 | ); 20 | 21 | //highlight rows 22 | this.table.selectAll('tr').classed('highlight', function(d) { 23 | var highlightedIds = dataListing.codebook.data.highlighted.map( 24 | m => m['web-codebook-index'] 25 | ); 26 | return highlightedIds.indexOf(d['web-codebook-index']) > -1; 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/codebook/defaultSettings.js: -------------------------------------------------------------------------------- 1 | const defaultSettings = { 2 | filters: [], 3 | groups: [], 4 | variableLabels: [], 5 | variableTypes: [], 6 | hiddenVariables: [], 7 | meta: [], 8 | autogroups: 5, //automatically include categorical vars with 2-5 levels in the groups dropdown 9 | autofilter: 10, //automatically make filters for categorical variables with 2-10 levels 10 | autobins: true, 11 | nBins: 100, 12 | levelSplit: 5, //cutpoint for # of levels to use levelPlot() renderer 13 | maxLevels: 100, //bar charts with more than maxLevels are hidden by default 14 | controlVisibility: 'visible', 15 | chartVisibility: 'minimized', 16 | tabs: ['codebook', 'listing', 'chartMaker', 'settings'], 17 | dataName: '', 18 | whiteSpaceAsMissing: true, 19 | missingValues: [null, NaN, undefined] 20 | }; 21 | 22 | export default defaultSettings; 23 | -------------------------------------------------------------------------------- /src/codebook/init.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Initialize codebook 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { select as d3select } from 'd3'; 6 | import clone from '../util/clone'; 7 | import indicateLoading from './util/indicateLoading'; 8 | 9 | export function init(data) { 10 | var settings = this.config; 11 | 12 | //create chart wrapper in specified div 13 | this.wrap = d3select(this.element) 14 | .append('div') 15 | .attr('class', 'web-codebook') 16 | .datum(this); // bind codebook object to codebook container so as to pass down to successive child elements 17 | 18 | // call the before callback (if any) 19 | this.events.init.call(this); 20 | 21 | //save raw data 22 | this.data.raw = clone(data); 23 | this.data.raw.forEach((d, i) => { 24 | d['web-codebook-index'] = i + 1; // define an index with which to identify records uniquely 25 | }); 26 | this.data.filtered = this.data.raw; //assume no filters active on init :/ 27 | this.data.highlighted = []; 28 | 29 | //settings and defaults 30 | this.util.setDefaults(this); 31 | this.layout(); 32 | 33 | indicateLoading(this, '.web-codebook .settings', () => { 34 | //prepare the data summaries 35 | this.data.makeSummary(this); 36 | 37 | //make the title 38 | this.title.init(this); 39 | 40 | //draw controls 41 | this.util.makeAutomaticFilters(this); 42 | this.util.makeAutomaticGroups(this); 43 | this.controls.init(this); 44 | 45 | //initialize nav, title and instructions 46 | this.nav.init(this); 47 | this.instructions.init(this); 48 | 49 | //call after event (if any) 50 | this.events.complete.call(this); 51 | 52 | //initialize and then draw the codebook 53 | this.summaryTable.draw(this); 54 | 55 | //initialize and then draw the data listing 56 | this.dataListing.init(this); 57 | 58 | //initialize the chart maker 59 | this.chartMaker.init(this); 60 | 61 | //initialize the settings 62 | this.settings.init(this); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/codebook/instructions.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define instructions object. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { init } from './instructions/init'; 6 | import { update } from './instructions/update'; 7 | 8 | export const instructions = { 9 | init: init, 10 | update: update 11 | }; 12 | -------------------------------------------------------------------------------- /src/codebook/instructions/chartToggle/init.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Initialize show/hide all charts toggles. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | //export function init(selector, data, vars, settings) { 6 | export function init(codebook) { 7 | //initialize the wrapper 8 | var selector = codebook.instructions.wrap 9 | .append('span') 10 | .attr('class', 'control chart-toggle') 11 | .classed('hidden', codebook.config.chartVisibility == 'hidden'); 12 | 13 | selector.append('small').text('Toggle Details: '); 14 | var showAllButton = selector 15 | .append('button') 16 | .text('Show All Details') 17 | .on('click', function() { 18 | codebook.wrap.selectAll('.variable-row').classed('hiddenDetails', false); 19 | codebook.wrap.selectAll('.row-toggle').html('▼'); 20 | }); 21 | 22 | var hideAllButton = selector 23 | .append('button') 24 | .text('Hide All Details') 25 | .on('click', function() { 26 | codebook.wrap.selectAll('.variable-row').classed('hiddenDetails', true); 27 | codebook.wrap.selectAll('.row-toggle').html('►'); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/codebook/instructions/init.js: -------------------------------------------------------------------------------- 1 | export function init(codebook) { 2 | //no action needed on init, just update to the current text 3 | codebook.instructions.update(codebook); 4 | } 5 | -------------------------------------------------------------------------------- /src/codebook/instructions/update.js: -------------------------------------------------------------------------------- 1 | import { init as initToggle } from './chartToggle/init'; 2 | 3 | export function update(codebook) { 4 | var activeTab = codebook.nav.tabs.filter(d => d.active)[0]; 5 | 6 | //add instructions text 7 | codebook.instructions.wrap.text(activeTab.instructions); 8 | 9 | //add tab-specific controls 10 | if (activeTab.key == 'codebook') { 11 | initToggle(codebook); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/codebook/layout.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Generate HTML containers. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | export function layout() { 6 | this.loadingIndicator = this.wrap 7 | .append('div') 8 | .attr('id', 'loading-indicator') 9 | .style('display', 'none'); 10 | 11 | this.loadingIndicator.append('div').attr('class', 'spinner'); 12 | 13 | this.statusWrap = this.wrap.append('div').attr('class', 'statusWrap section'); 14 | this.title.wrap = this.wrap.append('div').attr('class', 'title section'); 15 | this.nav.wrap = this.wrap.append('div').attr('class', 'wcb-nav section'); 16 | this.controls.wrap = this.wrap 17 | .append('div') 18 | .attr('class', 'controls section'); 19 | this.instructions.wrap = this.wrap 20 | .append('div') 21 | .attr('class', 'instructions section'); 22 | this.summaryTable.wrap = this.wrap 23 | .append('div') 24 | .attr('class', 'summaryTable section') 25 | .classed('hidden', false); 26 | 27 | this.summaryTable.summaryText = this.summaryTable.wrap 28 | .append('strong') 29 | .attr('class', 'summaryText section'); 30 | 31 | this.fileListing = {}; 32 | this.fileListing.wrap = this.wrap 33 | .append('div') 34 | .attr('class', 'fileListing section') 35 | .classed('hidden', true); 36 | 37 | this.dataListing.wrap = this.wrap 38 | .append('div') 39 | .attr('class', 'dataListing section') 40 | .classed('hidden', true); 41 | 42 | this.chartMaker.wrap = this.wrap 43 | .append('div') 44 | .attr('class', 'chartMaker section') 45 | .classed('hidden', true); 46 | 47 | this.settings.wrap = this.wrap 48 | .append('div') 49 | .attr('class', 'settings section') 50 | .classed('hidden', true); 51 | } 52 | -------------------------------------------------------------------------------- /src/codebook/nav.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define nav object. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { init } from './nav/init'; 6 | 7 | export const nav = { 8 | init: init 9 | }; 10 | -------------------------------------------------------------------------------- /src/codebook/nav/availableTabs.js: -------------------------------------------------------------------------------- 1 | const availableTabs = [ 2 | { 3 | key: 'files', 4 | label: 'Files', 5 | selector: '.fileListing', 6 | controls: false, 7 | instructions: 'Click a row to see the codebook for the file.' 8 | }, 9 | { 10 | key: 'codebook', 11 | label: 'Codebook', 12 | selector: '.summaryTable', 13 | controls: true, 14 | instructions: 'Automatically generated data summaries for each column.' 15 | }, 16 | { 17 | key: 'listing', 18 | label: 'Data Listing', 19 | selector: '.dataListing', 20 | controls: true, 21 | instructions: 'Listing of all selected records.' 22 | }, 23 | { 24 | key: 'chartMaker', 25 | label: 'Charts', 26 | selector: '.chartMaker', 27 | controls: true, 28 | instructions: 29 | 'Pick two variables to compare. Filter and group (panel) the chart using the controls above.' 30 | }, 31 | { 32 | key: 'settings', 33 | label: '⚙', 34 | selector: '.settings', 35 | controls: false, 36 | instructions: 37 | "This interactive table allows users to modify each column's metadata. Updating these settings will reset the codebook and data listing." 38 | } 39 | ]; 40 | 41 | export default availableTabs; 42 | -------------------------------------------------------------------------------- /src/codebook/nav/init.js: -------------------------------------------------------------------------------- 1 | import availableTabs from './availableTabs'; 2 | import { select as d3select } from 'd3'; 3 | import clone from '../../util/clone'; 4 | 5 | export function init(codebook) { 6 | const defaultTabs = clone(availableTabs); 7 | codebook.nav.wrap.selectAll('*').remove(); 8 | 9 | //permanently hide the codebook sections that aren't included 10 | defaultTabs.forEach(function(tab) { 11 | tab.wrap = codebook.wrap.select(tab.selector); 12 | tab.wrap.classed( 13 | 'hidden', 14 | codebook.config.tabs.map(m => m.key).indexOf(tab.key) == -1 15 | ); 16 | }); 17 | 18 | //get the tabs for the current codebook 19 | codebook.nav.tabs = defaultTabs.filter( 20 | tab => codebook.config.tabs.map(m => m.key).indexOf(tab.key) > -1 21 | ); 22 | 23 | //overwrite labels/instruction if specified by user 24 | codebook.nav.tabs.forEach(function(tab) { 25 | var settingsMatch = codebook.config.tabs.filter(f => f.key == tab.key)[0]; 26 | tab.label = settingsMatch.label || tab.label; 27 | tab.controls = settingsMatch.controls || tab.controls; 28 | tab.instructions = settingsMatch.instructions || tab.instructions; 29 | }); 30 | 31 | //set the active tabs 32 | codebook.nav.tabs.forEach(function(t) { 33 | t.active = t.key == codebook.config.defaultTab; 34 | t.wrap.classed('hidden', !t.active); 35 | }); 36 | 37 | //draw the nav 38 | if (codebook.nav.tabs.length > 1) { 39 | var chartNav = codebook.nav.wrap 40 | .append('ul') 41 | .attr('class', 'wcb-nav wcb-nav-tabs'); 42 | var navItems = chartNav 43 | .selectAll('li') 44 | .data(codebook.nav.tabs) //make this a setting 45 | .enter() 46 | .append('li') 47 | .attr('class', d => d.key) 48 | .classed('active', function(d, i) { 49 | return d.active; //make this a setting 50 | }) 51 | .attr('title', d => `View ${d.key}`); 52 | 53 | navItems.append('a').html(function(d) { 54 | return d.label; 55 | }); 56 | 57 | //event listener for nav clicks 58 | navItems.on('click', function(d) { 59 | if (!d.active) { 60 | codebook.nav.tabs.forEach(function(t) { 61 | t.active = d.label == t.label; //set the clicked tab to active 62 | navItems.filter(f => f == t).classed('active', t.active); //style the active nav element 63 | t.wrap.classed('hidden', !t.active); //hide all of the wraps (except for the active one) 64 | }); 65 | 66 | codebook.instructions.update(codebook); 67 | 68 | //show/hide the controls (unless they are disabled) 69 | if (codebook.config.controlVisibility !== 'hidden') 70 | codebook.config.previousControlVisibility = 71 | codebook.config.controlVisibility; 72 | if (codebook.config.controlVisibility != 'disabled') { 73 | codebook.config.controlVisibility = d.controls 74 | ? codebook.config.previousControlVisibility 75 | : 'hidden'; 76 | codebook.controls.controlToggle.set(codebook); 77 | } 78 | } 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/codebook/settings.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define settings object. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { init } from './settings/init'; 6 | import { layout } from './settings/layout'; 7 | 8 | export const settings = { 9 | init: init, 10 | layout: layout 11 | }; 12 | -------------------------------------------------------------------------------- /src/codebook/settings/init.js: -------------------------------------------------------------------------------- 1 | import { select as d3select } from 'd3'; 2 | import indicateLoading from '../util/indicateLoading'; 3 | 4 | export function init(codebook) { 5 | indicateLoading(codebook, '.web-codebook .settings .column-table'); 6 | 7 | codebook.settings.layout(codebook); 8 | } 9 | -------------------------------------------------------------------------------- /src/codebook/settings/layout.js: -------------------------------------------------------------------------------- 1 | import { select as d3select } from 'd3'; 2 | import updateSettings from './updateSettings'; 3 | 4 | export function layout(codebook) { 5 | //Create list of columns in the data file. 6 | const columns = codebook.data.summary.map(d => d.value_col), 7 | groupColumns = codebook.config.groups.map(d => d.value_col), 8 | filterColumns = codebook.config.filters.map(d => d.value_col), 9 | hiddenColumns = codebook.config.hiddenVariables, 10 | labeledColumns = codebook.config.variableLabels.map(d => d.value_col), 11 | typedColumns = codebook.config.variableTypes.map(d => d.value_col), 12 | columnTableColumns = ['Column', 'Label', 'Type', 'Group', 'Filter', 'Hide'], 13 | columnMetadata = columns.map(column => { 14 | const columnDatum = { 15 | Column: column, 16 | Label: { 17 | type: 'text', 18 | value: 19 | labeledColumns.indexOf(column) > -1 20 | ? codebook.config.variableLabels[labeledColumns.indexOf(column)] 21 | .label 22 | : '' 23 | }, 24 | Type: { 25 | type: 'text', 26 | value: 27 | typedColumns.indexOf(column) > -1 28 | ? codebook.config.variableTypes[typedColumns.indexOf(column)].type 29 | : '', 30 | autoType: codebook.data.summary.filter(f => f.value_col == column)[0] 31 | .type 32 | }, 33 | Group: { 34 | type: 'checkbox', 35 | checked: groupColumns.indexOf(column) > -1 36 | }, 37 | Filter: { 38 | type: 'checkbox', 39 | checked: filterColumns.indexOf(column) > -1 40 | }, 41 | Hide: { 42 | type: 'checkbox', 43 | checked: hiddenColumns.indexOf(column) > -1 44 | } 45 | }; 46 | 47 | return columnDatum; 48 | }), 49 | //define table 50 | columnTable = codebook.settings.wrap 51 | .append('table') 52 | .classed('column-table', true), 53 | //define table headers 54 | columnTableHeader = columnTable.append('thead').append('tr'), 55 | columnTableHeaders = columnTableHeader 56 | .selectAll('th') 57 | .data(columnTableColumns) 58 | .enter() 59 | .append('th') 60 | .attr('class', d => d) 61 | .text(d => d), 62 | //define table rows 63 | columnTableRows = columnTable 64 | .append('tbody') 65 | .selectAll('tr') 66 | .data(columnMetadata) 67 | .enter() 68 | .append('tr') 69 | .classed('hidden', d => d.Column === 'web-codebook-index'), 70 | columnTableCells = columnTableRows 71 | .selectAll('td') 72 | .data(d => 73 | Object.keys(d).map(di => { 74 | return { column: d.Column, key: di, value: d[di] }; 75 | }) 76 | ) 77 | .enter() 78 | .append('td') 79 | .attr('class', d => d.key) 80 | .each(function(d, i) { 81 | const cell = d3select(this); 82 | 83 | switch (d.key) { 84 | case 'Column': 85 | cell.text(d.value); 86 | break; 87 | case 'Label': 88 | cell.attr('title', 'Define variable label'); 89 | cell 90 | .append('input') 91 | .attr('type', d.value.type) 92 | .property('value', d.value.value) 93 | .on('change', () => updateSettings(codebook, d.key)); 94 | break; 95 | case 'Type': 96 | cell.attr('title', 'Specify Variable Type'); 97 | const typeSelect = cell 98 | .append('select') 99 | .on('change', () => updateSettings(codebook, d.key)); 100 | var typeOptions = [ 101 | 'automatic (' + d.value.autoType + ')', 102 | 'continuous', 103 | 'categorical' 104 | ]; 105 | 106 | typeSelect 107 | .selectAll('option') 108 | .data(typeOptions) 109 | .enter() 110 | .append('option') 111 | .property('selected', opt => opt == d.value.value) 112 | .text(opt => opt); 113 | break; 114 | default: 115 | cell.attr( 116 | 'title', 117 | `${d.value.checked ? 'Remove' : 'Add'} ${d.column} ${ 118 | d.value.checked ? 'from' : 'to' 119 | } ${d.key.toLowerCase()} list` 120 | ); 121 | const checkbox = cell 122 | .append('input') 123 | .attr('type', d.value.type) 124 | .property('checked', d.value.checked) 125 | .on('change', () => updateSettings(codebook, d.key)); 126 | } 127 | }); 128 | } 129 | -------------------------------------------------------------------------------- /src/codebook/settings/updateSettings.js: -------------------------------------------------------------------------------- 1 | import { select as d3select } from 'd3'; 2 | import reset from './updateSettings/reset'; 3 | 4 | export default function updateSettings(codebook, column) { 5 | const setting = 6 | column === 'Label' 7 | ? 'variableLabels' 8 | : column === 'Group' 9 | ? 'groups' 10 | : column === 'Filter' 11 | ? 'filters' 12 | : column === 'Hide' 13 | ? 'hiddenVariables' 14 | : column === 'Type' 15 | ? 'variableTypes' 16 | : console.warn('Something unsetting has occurred...'); 17 | const inputs = codebook.settings.wrap.selectAll(`.column-table td.${column}`); 18 | if (['Group', 'Filter', 'Hide'].indexOf(column) > -1) { 19 | //redefine settings array 20 | codebook.config[setting] = inputs 21 | .filter(function() { 22 | return d3select(this) 23 | .select('input') 24 | .property('checked'); 25 | }) 26 | .data() 27 | .map(d => { 28 | return column !== 'Hide' ? { value_col: d.column } : d.column; 29 | }); 30 | } else if (['Label', 'Type'].indexOf(column) > -1) { 31 | //redefine settings array 32 | var inputType = column == 'Label' ? 'input' : 'select'; 33 | var currentValues = inputs 34 | .filter(function(d) { 35 | d.value.value = d3select(this) 36 | .select(inputType) 37 | .property('value'); 38 | return d.value.value !== ''; 39 | }) 40 | .data() 41 | .map(d => { 42 | var obj = { value_col: d.column }; 43 | obj[column.toLowerCase()] = d.value.value; 44 | return obj; 45 | }); 46 | if (column == 'Type') { 47 | currentValues = currentValues.filter(f => f.type.slice(0, 4) != 'auto'); 48 | } 49 | codebook.config[setting] = currentValues; 50 | } 51 | 52 | //reset 53 | reset(codebook); 54 | } 55 | -------------------------------------------------------------------------------- /src/codebook/settings/updateSettings/reset.js: -------------------------------------------------------------------------------- 1 | import indicateLoading from '../../util/indicateLoading'; 2 | 3 | export default function reset(codebook) { 4 | indicateLoading(codebook, '.web-codebook .dataListing .wc-chart', () => { 5 | //remove grouping and select 'None' group option 6 | delete codebook.config.group; 7 | codebook.controls.groups.update(codebook); 8 | codebook.controls.wrap 9 | .select('.group-select') 10 | .selectAll('option') 11 | .property('selected', d => d.value_col === 'None'); 12 | 13 | //remove filtering and select all filter options 14 | codebook.data.highlighted = []; 15 | codebook.data.filtered = codebook.data.raw; 16 | codebook.controls.filters.update(codebook); 17 | codebook.controls.wrap 18 | .selectAll('.filterCustom option') 19 | .property('selected', true); 20 | 21 | //redraw data summary, codebook, and listing. 22 | codebook.data.makeSummary(codebook); 23 | codebook.title.updateCountSummary(codebook); 24 | codebook.summaryTable.draw(codebook); 25 | codebook.dataListing.init(codebook); 26 | codebook.chartMaker.init(codebook); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/codebook/summaryTable.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define summaryTable object (the meat and potatoes). 3 | \------------------------------------------------------------------------------------------------*/ 4 | import { draw } from './summaryTable/draw'; 5 | import { renderRow } from './summaryTable/renderRow'; 6 | 7 | export const summaryTable = { 8 | draw: draw, 9 | renderRow: renderRow 10 | }; 11 | -------------------------------------------------------------------------------- /src/codebook/summaryTable/draw.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | draw/update the summaryTable 3 | \------------------------------------------------------------------------------------------------*/ 4 | import indicateLoading from '../util/indicateLoading'; 5 | 6 | export function draw(codebook) { 7 | /* 8 | indicateLoading( 9 | codebook, 10 | '.web-codebook .summaryTable .variable-row .row-title' 11 | ); 12 | */ 13 | 14 | //enter/update/exit for variableDivs 15 | //BIND the newest data 16 | var varRows = codebook.summaryTable.wrap 17 | .selectAll('div.variable-row') 18 | .data(codebook.data.summary, d => d.value_col); 19 | 20 | //ENTER 21 | varRows 22 | .enter() 23 | .append('div') 24 | .attr('class', function(d) { 25 | return 'variable-row ' + d.type; 26 | }); 27 | 28 | //Hide variable rows corresponding to variables specified in settings.hiddenVariables. 29 | varRows.classed( 30 | 'hidden', 31 | d => codebook.config.hiddenVariables.indexOf(d.value_col) > -1 32 | ); 33 | 34 | //Set chart visibility (on initial load only - then keep user settings) 35 | if (codebook.config.chartVisibility != 'user-defined') { 36 | varRows.classed( 37 | 'hiddenDetails', 38 | codebook.config.chartVisibility != 'visible' 39 | ); 40 | } 41 | 42 | codebook.config.chartVisibility = 43 | codebook.config.chartVisibility == 'hidden' ? 'hidden' : 'user-defined'; 44 | 45 | //ENTER + Update 46 | varRows.each(codebook.summaryTable.renderRow); 47 | 48 | //EXIT 49 | varRows.exit().remove(); 50 | 51 | codebook.summaryTable.wrap.selectAll('div.status.error').remove(); 52 | if (varRows[0].length == 0) { 53 | codebook.summaryTable.wrap 54 | .append('div') 55 | .attr('class', 'status error') 56 | .text( 57 | 'No values selected. Update the filters above or load a different data set.' 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/codebook/summaryTable/renderRow.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Intialize the summary table 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import makeChart from './renderRow/makeChart.js'; 6 | import makeDetails from './renderRow/makeDetails.js'; 7 | import makeMeta from './renderRow/makeMeta.js'; 8 | import makeTitle from './renderRow/makeTitle.js'; 9 | 10 | import { select as d3select } from 'd3'; 11 | 12 | export function renderRow(d) { 13 | var rowWrap = d3select(this); 14 | rowWrap.selectAll('*').remove(); 15 | 16 | rowWrap 17 | .append('div') 18 | .attr('class', 'row-head section') 19 | .append('div') 20 | .attr('class', 'row-title') 21 | .each(makeTitle); 22 | 23 | rowWrap 24 | .append('div') 25 | .attr('class', 'row-details section') 26 | .each(makeDetails); 27 | 28 | rowWrap 29 | .append('div') 30 | .attr('class', 'row-chart section') 31 | .each(makeChart); 32 | 33 | rowWrap 34 | .append('div') 35 | .attr('class', 'row-meta section') 36 | .each(makeMeta); 37 | } 38 | -------------------------------------------------------------------------------- /src/codebook/summaryTable/renderRow/details/clearDetails.js: -------------------------------------------------------------------------------- 1 | export default function clearDetails(d, list) { 2 | list.selectAll('*').remove(); 3 | } 4 | -------------------------------------------------------------------------------- /src/codebook/summaryTable/renderRow/details/detailList.js: -------------------------------------------------------------------------------- 1 | import renderValues from './renderValues'; 2 | import renderStats from './renderStats'; 3 | import renderMeta from './renderMeta'; 4 | import clearDetails from './clearDetails'; 5 | 6 | const detailList = [ 7 | { key: 'Stats', action: renderStats }, 8 | { key: 'Meta', action: renderMeta }, 9 | { key: 'Values', action: renderValues }, 10 | { key: 'None', action: clearDetails } 11 | ]; 12 | export default detailList; 13 | -------------------------------------------------------------------------------- /src/codebook/summaryTable/renderRow/details/renderMeta.js: -------------------------------------------------------------------------------- 1 | //Render metadata 2 | export default function renderMeta(d, list) { 3 | list.selectAll('*').remove(); 4 | 5 | // don't renderer items with no 6 | var dropped = []; 7 | d.meta.forEach(function(d) { 8 | if (!d.value) { 9 | d.hidden = true; 10 | dropped.push(' "' + d.key + '"'); 11 | } 12 | }); 13 | 14 | //render the items 15 | var metaItems = list 16 | .selectAll('li.meta') 17 | .data(d.meta.filter(f => f.key != 'Type')) 18 | .enter() 19 | .append('li') 20 | .classed('meta', true) 21 | .classed('hidden', d => d.hidden); 22 | 23 | metaItems 24 | .append('div') 25 | .text(d => d.key) 26 | .attr('class', 'wcb-label'); 27 | metaItems 28 | .append('div') 29 | .text(d => d.value) 30 | .attr('class', 'value'); 31 | 32 | if (dropped.length) { 33 | list 34 | .append('li') 35 | .attr('class', 'details') 36 | .append('div') 37 | .html('ⓘ') 38 | .property( 39 | 'title', 40 | 'Meta data for ' + 41 | dropped.length + 42 | ' item(s) (' + 43 | dropped.toString() + 44 | ') were empty and are hidden.' 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/codebook/summaryTable/renderRow/details/renderStats.js: -------------------------------------------------------------------------------- 1 | //Render Summary Stats 2 | export default function renderStats(d, list) { 3 | var ignoreStats = [ 4 | 'values', 5 | 'highlightValues', 6 | 'min', 7 | 'max', 8 | 'n', 9 | 'N', 10 | 'nMissing', 11 | 'percentMissing' 12 | ]; 13 | 14 | var statNames = Object.keys(d.statistics) 15 | .filter(f => ignoreStats.indexOf(f) === -1) //remove value lists 16 | .filter(f => f.indexOf('ile') === -1); //remove "percentiles" 17 | 18 | var statList = statNames.map(stat => { 19 | return { 20 | key: stat !== 'missingSummary' ? stat : 'Missing', 21 | value: d.statistics[stat] 22 | }; 23 | }); 24 | 25 | var stats = list 26 | .selectAll('li.stat') 27 | .data(statList) 28 | .enter() 29 | .append('li') 30 | .attr('class', 'stat'); 31 | stats 32 | .append('div') 33 | .text(d => d.key) 34 | .attr('class', 'wcb-label'); 35 | stats 36 | .append('div') 37 | .text(d => d.value) 38 | .attr('class', 'value'); 39 | } 40 | -------------------------------------------------------------------------------- /src/codebook/summaryTable/renderRow/details/renderValues.js: -------------------------------------------------------------------------------- 1 | import { 2 | select as d3select, 3 | format as d3format, 4 | set as d3set, 5 | merge as d3merge 6 | } from 'd3'; 7 | 8 | export default function renderValues(d, list) { 9 | //make a list of values 10 | if (d.type == 'categorical') { 11 | var topValues = d.statistics.values 12 | .sort(function(a, b) { 13 | return b.n - a.n; 14 | }) 15 | .filter(function(d, i) { 16 | return i < 5; 17 | }); 18 | 19 | var valueItems = list 20 | .selectAll('li.value') 21 | .data(topValues) 22 | .enter() 23 | .append('li') 24 | .attr('class', 'value'); 25 | 26 | valueItems 27 | .append('div') 28 | .text(d => d.key) 29 | .attr('class', 'wcb-label') 30 | .attr('title', d => d.key); 31 | 32 | valueItems 33 | .append('div') 34 | .text(d => d.n + ' (' + d3format('0.1%')(d.prop_n) + ')') 35 | .attr('class', 'value'); 36 | 37 | if (d.statistics.values.length > 5) { 38 | var totLength = d.statistics.values.length; 39 | var extraCount = totLength - 5; 40 | var extra_span = list 41 | .append('li') 42 | .attr('class', 'value') 43 | .append('div') 44 | .attr('class', 'wcb-label') 45 | .html('and ' + extraCount + ' more.'); 46 | } 47 | } else if (d.type == 'continuous') { 48 | let nonMissing = d.values.filter(f => !f.missing).map(m => +m.value); 49 | var sortedValues = d3set(nonMissing) 50 | .values() //get unique 51 | .sort(function(a, b) { 52 | return a - b; 53 | }); // sort low to high 54 | 55 | if (sortedValues.length > 6) { 56 | var minValues = sortedValues.filter(function(d, i) { 57 | return i < 3; 58 | }); 59 | var nValues = sortedValues.length; 60 | var maxValues = sortedValues.filter(function(d, i) { 61 | return i >= nValues - 3; 62 | }); 63 | var valList = d3merge([minValues, ['...'], maxValues]); 64 | } else { 65 | var valList = sortedValues; 66 | } 67 | var valueItems = list 68 | .selectAll('li.value') 69 | .data(valList) 70 | .enter() 71 | .append('li') 72 | .attr('class', 'value'); 73 | 74 | valueItems 75 | .append('div') 76 | .attr('class', 'wcb-label') 77 | .text(function(d, i) { 78 | return i == 0 ? 'Min' : i == valList.length - 1 ? 'Max' : ' '; 79 | }); 80 | valueItems 81 | .append('div') 82 | .attr('class', 'value') 83 | .text(d => d) 84 | .attr('title', d => (d == '...' ? nValues - 6 + ' other values' : '')) 85 | .style('cursor', d => (d == '...' ? 'help' : null)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/codebook/summaryTable/renderRow/makeChart.js: -------------------------------------------------------------------------------- 1 | import { charts } from '../../../charts'; 2 | import { select as d3select } from 'd3'; 3 | 4 | export default function makeChart(d) { 5 | //Common chart settings 6 | this.height = 100; 7 | this.margin = { right: 200, left: 30 }; 8 | if (d.statistics.n > 0) { 9 | if (d.chartType === 'horizontalBars') { 10 | charts.createHorizontalBarsControls(this, d); 11 | charts.createHorizontalBars(this, d); 12 | } else if (d.chartType === 'verticalBars') { 13 | charts.createVerticalBarsControls(this, d); 14 | charts.createVerticalBars(this, d); 15 | } else if (d.chartType === 'character') { 16 | let summary = d3select(this) 17 | .append('div') 18 | .attr('class', 'characterSummary') 19 | .html(d.summaryText); 20 | 21 | summary.select('span.drawLevel').on('click', function() { 22 | let node = this.parentNode.parentNode.parentNode; 23 | d3.select(node) 24 | .select('div.characterSummary') 25 | .remove(); 26 | charts.createVerticalBarsControls(node, d); 27 | charts.createVerticalBars(node, d); 28 | }); 29 | } else if (d.chartType === 'histogramBoxPlot') { 30 | charts.createHistogramBoxPlotControls(this, d); 31 | charts.createHistogramBoxPlot(this, d); 32 | } else { 33 | console.warn('Invalid chart type for ' + d.key); 34 | } 35 | } else { 36 | d3select(this) 37 | .append('div') 38 | .attr('class', 'missingText') 39 | .text('All values missing.'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/codebook/summaryTable/renderRow/makeDetails.js: -------------------------------------------------------------------------------- 1 | import { select as d3select } from 'd3'; 2 | import detailList from './details/detailList'; 3 | import renderStats from './details/renderStats'; 4 | import renderValues from './details/renderValues'; 5 | 6 | export default function makeDetails(d) { 7 | var stat_list = d3select(this) 8 | .append('ul') 9 | .attr('class', 'stats'); 10 | var val_list = d3select(this) 11 | .append('ul') 12 | .attr('class', 'values'); 13 | 14 | var parent = d3select(this.parentNode.parentNode); 15 | 16 | //render stats & values on initial load 17 | renderStats(d, stat_list); 18 | renderValues(d, val_list); 19 | } 20 | -------------------------------------------------------------------------------- /src/codebook/summaryTable/renderRow/makeMeta.js: -------------------------------------------------------------------------------- 1 | import { select as d3select } from 'd3'; 2 | import renderMeta from './details/renderMeta'; 3 | 4 | export default function makeMeta(d) { 5 | var hasMeta = 6 | d.meta.filter(f => !f.hidden).filter(f => f.key.toLowerCase() != 'type') 7 | .length > 0; 8 | if (hasMeta) { 9 | var meta_list = d3select(this) 10 | .append('ul') 11 | .attr('class', 'meta'); 12 | 13 | var parent = d3select(this.parentNode.parentNode); 14 | renderMeta(d, meta_list); 15 | } else { 16 | d3select(this).style('display', 'none'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/codebook/summaryTable/renderRow/makeTitle.js: -------------------------------------------------------------------------------- 1 | import { select as d3select } from 'd3'; 2 | import { format as d3format } from 'd3'; 3 | import createSpark from '../../../charts/createSpark'; 4 | 5 | export default function makeTitle(d) { 6 | var rowDiv = d3select(this.parentNode.parentNode.parentNode); 7 | var chartDiv = rowDiv.select('.row-chart'); 8 | var hiddenFlag = rowDiv.classed('hiddenDetails'); 9 | 10 | //Add row toggle 11 | d3select(this) 12 | .append('div') 13 | .attr('class', 'row-toggle') 14 | .html(hiddenFlag ? '▼' : '►') 15 | .classed('hidden', function(d) { 16 | return d.chartVisibility == 'hidden'; 17 | }) 18 | .on('click', function() { 19 | var rowDiv = d3select(this.parentNode.parentNode.parentNode); 20 | var chartDiv = rowDiv.select('.row-chart'); 21 | var hiddenFlag = rowDiv.classed('hiddenDetails'); 22 | rowDiv.classed('hiddenDetails', !hiddenFlag); 23 | d3select(this).html(hiddenFlag ? '▼' : '►'); 24 | }); 25 | 26 | //add variable name in quotes 27 | d3select(this) 28 | .append('span') 29 | .attr('class', 'title-span') 30 | .text(d => "'" + d.value_col + "'"); 31 | 32 | //add variable label (if any) 33 | if (d.value_col != d.label) { 34 | d3select(this) 35 | .append('span') 36 | .attr('class', 'label-span') 37 | .text(d => d.label); 38 | } 39 | 40 | //add variable type 41 | /* 42 | d3select(this) 43 | .append('span') 44 | .attr('class', 'type') 45 | .text(d => d.type); 46 | */ 47 | 48 | //add sparklines 49 | var sparkDiv = d3select(this) 50 | .append('div') 51 | .attr('class', 'spark') 52 | .datum(d); 53 | 54 | if (d.chartType != 'character') { 55 | sparkDiv.each(createSpark); 56 | } 57 | 58 | let type = 59 | d.type == 'continuous' 60 | ? 'continuous' 61 | : d.chartType == 'character' 62 | ? 'character' 63 | : 'categorical'; 64 | sparkDiv 65 | .append('div') 66 | .attr('class', 'sparkLabel') 67 | .text(type == 'continuous' ? '#' : type == 'character' ? 'abc' : 'cat') 68 | .attr('title', type + ' column'); 69 | 70 | //add percent missing (if > 0%) 71 | d3select(this) 72 | .append('span') 73 | .attr('class', 'percent-missing') 74 | .text(d => d3format('0.1%')(d.statistics.percentMissing) + ' missing') 75 | .style('display', d => (d.statistics.percentMissing == 0 ? 'none' : null)) 76 | .style('cursor', 'pointer') 77 | .style('color', d => (d.statistics.percentMissing >= 0.1 ? 'red' : '#999')) 78 | .attr( 79 | 'title', 80 | d => 81 | d.statistics.nMissing + 82 | ' of ' + 83 | d.statistics.N + 84 | ' missing. Missing values include:\n' + 85 | d.missingSummary 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/codebook/title.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define title object. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { init } from './title/init'; 6 | import { highlight } from './title/highlight'; 7 | import { updateCountSummary } from './title/updateCountSummary'; 8 | 9 | export const title = { 10 | init: init, 11 | highlight: highlight, 12 | updateCountSummary: updateCountSummary 13 | }; 14 | -------------------------------------------------------------------------------- /src/codebook/title/highlight.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define clear highlighting button object. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { init } from './highlight/init'; 6 | 7 | export const highlight = { init: init }; 8 | -------------------------------------------------------------------------------- /src/codebook/title/highlight/init.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Initialize clear highlighting button. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | export function init(codebook) { 6 | //initialize the wrapper 7 | codebook.title.highlight.clearButton = codebook.title.wrap 8 | .append('button') 9 | .classed('clear-highlight', true) 10 | .classed('hidden', codebook.data.highlighted.length == 0) 11 | .text('Clear Highlighting') 12 | .on('click', function() { 13 | codebook.data.highlighted = []; 14 | 15 | codebook.data.makeSummary(codebook); 16 | codebook.dataListing.init(codebook); 17 | codebook.summaryTable.draw(codebook); 18 | codebook.chartMaker.draw(codebook); 19 | codebook.title.updateCountSummary(codebook); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/codebook/title/init.js: -------------------------------------------------------------------------------- 1 | export function init(codebook) { 2 | codebook.title.fileWrap = codebook.title.wrap 3 | .append('span') 4 | .attr('class', 'file') 5 | .text( 6 | codebook.config.dataName 7 | ? codebook.config.dataName + ' Codebook' 8 | : 'Codebook' 9 | ); 10 | 11 | codebook.title.countSummary = codebook.title.wrap 12 | .append('span') 13 | .attr('class', 'countSummary'); 14 | 15 | codebook.title.highlight.init(codebook); 16 | 17 | codebook.title.updateCountSummary(codebook); 18 | } 19 | -------------------------------------------------------------------------------- /src/codebook/title/updateCountSummary.js: -------------------------------------------------------------------------------- 1 | import { format as d3format } from 'd3'; 2 | 3 | export function updateCountSummary(codebook) { 4 | var warn = false; 5 | //get number of rows shown 6 | if (codebook.data.summary.length > 0) { 7 | var nShown = codebook.data.summary[0].statistics.N; 8 | var nTot = codebook.data.raw.length; 9 | var percent = d3format('0.1%')(nShown / nTot); 10 | var rowSummary = 11 | nShown + ' of ' + nTot + ' (' + percent + ') rows selected'; 12 | } else { 13 | var rowSummary = 'No rows selected.'; 14 | warn = true; 15 | } 16 | 17 | //Add note regarding highlighted cells and show/hide the clear highlight button 18 | var highlightSummary = 19 | codebook.data.highlighted.length > 0 20 | ? ' and ' + 21 | codebook.data.highlighted.length + 22 | ' highlighted. ' 23 | : '.'; 24 | 25 | codebook.title.highlight.clearButton.classed( 26 | 'hidden', 27 | codebook.data.highlighted.length == 0 28 | ); 29 | 30 | //get number of columns hidden 31 | var nCols_sub = codebook.data.summary.filter(d => !d.hidden).length; 32 | var nCols_all = codebook.data.summary.length - 1; //-1 is for the index var 33 | var nCols_diff = nCols_all - nCols_sub; 34 | //var percent = d3format('0.1%')(nCols_sub / nCols_all); 35 | var colSummary = nCols_diff > 0 ? nCols_diff + ' columns hidden' : ''; 36 | 37 | var tableSummary = rowSummary + highlightSummary + ' ' + colSummary; 38 | 39 | codebook.title.countSummary.html(tableSummary); 40 | } 41 | -------------------------------------------------------------------------------- /src/codebook/util.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define util object. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { setDefaults } from './util/setDefaults'; 6 | import { makeAutomaticFilters } from './util/makeAutomaticFilters'; 7 | import { makeAutomaticGroups } from './util/makeAutomaticGroups'; 8 | import { getBinCounts } from './util/getBinCounts'; 9 | 10 | export const util = { 11 | setDefaults: setDefaults, 12 | makeAutomaticFilters: makeAutomaticFilters, 13 | makeAutomaticGroups: makeAutomaticGroups, 14 | getBinCounts: getBinCounts 15 | }; 16 | -------------------------------------------------------------------------------- /src/codebook/util/getBinCounts.js: -------------------------------------------------------------------------------- 1 | // determine the number of bins to use in the histogram based on the data. 2 | // Based on an implementation of the Freedman-Diaconis 3 | // See https://en.wikipedia.org/wiki/Freedman%E2%80%93Diaconis_rule for more 4 | // values should be an array of numbers 5 | 6 | import defaultSettings from '../defaultSettings'; 7 | 8 | export function getBinCounts(codebook) { 9 | //function to set the bin count for a single variable 10 | function setBinCount(summaryData) { 11 | //Freedman-Diaconis rule - returns the recommended bin size for a histogram 12 | function FreedmanDiaconis(IQR, n) { 13 | var cubeRootN = Math.pow(n, 1.0 / 3.0); 14 | return 2 * (IQR / cubeRootN); 15 | } 16 | 17 | var IQR = 18 | +summaryData.statistics['3rd quartile'] - 19 | +summaryData.statistics['1st quartile']; 20 | var n = summaryData.statistics['n']; 21 | var range = +summaryData.statistics['max'] - +summaryData.statistics['min']; 22 | var binSize = FreedmanDiaconis(IQR, n); 23 | var bins = 24 | binSize > 0 25 | ? Math.ceil(range / binSize) 26 | : codebook.config.nBins > 0 27 | ? codebook.config.nBins 28 | : defaultSettings.nBins; 29 | 30 | return bins; 31 | } 32 | 33 | var continuousVars = codebook.data.summary.filter( 34 | d => d.type == 'continuous' 35 | ); 36 | continuousVars.forEach(function(cvar) { 37 | cvar.bins = codebook.config.autoBins 38 | ? codebook.config.nBins 39 | : setBinCount(cvar); 40 | if (Object.keys(codebook.config).indexOf('group') > -1) { 41 | cvar.groups.forEach(function(gvar) { 42 | gvar.bins = codebook.config.autoBins 43 | ? codebook.config.nBins 44 | : setBinCount(gvar); 45 | }); 46 | } 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/codebook/util/indicateLoading.js: -------------------------------------------------------------------------------- 1 | import { select as d3select } from 'd3'; 2 | 3 | export default function indicateLoading(codebook, element, callback) { 4 | codebook.statusWrap.selectAll('*').remove(); 5 | codebook.loadingIndicator.style('display', 'block'); 6 | //wait until the loading indicator is visible 7 | const loading = setInterval(() => { 8 | try { 9 | const laidOut = d3select(element).property('offsetwidth') > 0, 10 | displayNone = d3select(element).style('display') === 'none'; 11 | 12 | //loading is complete 13 | if (!(laidOut && displayNone)) { 14 | if (callback) callback(); 15 | clearInterval(loading); 16 | codebook.loadingIndicator.style('display', 'none'); 17 | d3select('#loading-text').remove(); 18 | } 19 | } catch (err) { 20 | clearInterval(loading); 21 | codebook.loadingIndicator.style('display', 'none'); 22 | d3select('#loading-text').remove(); 23 | 24 | codebook.statusWrap 25 | .append('div') 26 | .attr('class', 'status error') 27 | .html('There was a problem updating the chart:
' + err); 28 | 29 | console.warn(err); 30 | } 31 | }, 25); 32 | } 33 | -------------------------------------------------------------------------------- /src/codebook/util/makeAutomaticFilters.js: -------------------------------------------------------------------------------- 1 | export function makeAutomaticFilters(codebook) { 2 | //make filters for all categorical variables with less than autofilter levels 3 | if (codebook.config.autofilter > 1) { 4 | var autofilters = codebook.data.summary 5 | .filter(f => f.type == 'categorical') //categorical filters only 6 | .filter(f => f.statistics.values.length <= codebook.config.autofilter) //no huge filters 7 | .filter(f => f.statistics.values.length > 1) //no silly 1 item filters 8 | .map(function(m) { 9 | return { value_col: m.value_col }; 10 | }); 11 | 12 | codebook.config.filters = autofilters.length > 0 ? autofilters : []; 13 | } 14 | 15 | codebook.data.summary.forEach(variable => { 16 | variable.filter = 17 | codebook.config.filters 18 | .map(filter => filter.value_col) 19 | .indexOf(variable.value_col) > -1; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/codebook/util/makeAutomaticGroups.js: -------------------------------------------------------------------------------- 1 | export function makeAutomaticGroups(codebook) { 2 | //make groups for all categorical variables with less than autofilter levels 3 | if (codebook.config.autogroups > 1) { 4 | var autogroups = codebook.data.summary 5 | .filter(f => f.type == 'categorical') //categorical filters only 6 | .filter(f => f.statistics.values.length <= codebook.config.autogroups) //no groups 7 | .filter(f => f.statistics.values.length > 1) //no silly 1 item groups 8 | .map(function(m) { 9 | return { value_col: m.value_col }; 10 | }); 11 | 12 | codebook.config.groups = autogroups.length > 0 ? autogroups : []; 13 | } 14 | 15 | codebook.data.summary.forEach(variable => { 16 | variable.groupOption = 17 | codebook.config.groups 18 | .map(group => group.value_col) 19 | .indexOf(variable.value_col) > -1; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/codebook/util/setDefaults.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '../defaultSettings'; 2 | import availableTabs from '../nav/availableTabs'; 3 | 4 | export function setDefaults(codebook) { 5 | /**************** Column Metadata ************/ 6 | codebook.config.meta = codebook.config.meta || defaultSettings.meta; 7 | 8 | // If labels are specified in the metadata, use them as the default 9 | if (codebook.config.meta.length) { 10 | var metaLabels = []; 11 | codebook.config.meta.forEach(function(m) { 12 | var mKeys = Object.keys(m).map(m => m.toLowerCase()); 13 | if ((mKeys.indexOf('value_col') > -1) & (mKeys.indexOf('label') > -1)) { 14 | metaLabels.push({ value_col: m['value_col'], label: m['label'] }); 15 | } 16 | }); 17 | defaultSettings.variableLabels = metaLabels; 18 | 19 | // If types are specified in the metadata, use them as the default 20 | var metaTypes = []; 21 | codebook.config.meta.forEach(function(m) { 22 | var mKeys = Object.keys(m); 23 | if ((mKeys.indexOf('value_col') > -1) & (mKeys.indexOf('type') > -1)) { 24 | if (['categorical', 'continuous'].indexOf(m.type.toLowerCase()) > -1) { 25 | metaTypes.push({ 26 | value_col: m['value_col'], 27 | type: m['type'].toLowerCase() 28 | }); 29 | } else { 30 | console.log( 31 | "Invalid type ('" + 32 | m.type + 33 | "') for " + 34 | m.value_col + 35 | ' specified in metadata.' 36 | ); 37 | } 38 | } 39 | }); 40 | defaultSettings.variableTypes = metaTypes; 41 | } 42 | 43 | /********************* Filter Settings *********************/ 44 | codebook.config.filters = codebook.config.filters || defaultSettings.filters; 45 | codebook.config.filters = codebook.config.filters.map(function(d) { 46 | if (typeof d == 'string') return { value_col: d }; 47 | else return d; 48 | }); 49 | 50 | //autofilter - don't use automatic filter if user specifies filters object 51 | codebook.config.autofilter = 52 | codebook.config.filters.length > 0 53 | ? false 54 | : codebook.config.autofilter == null 55 | ? defaultSettings.autofilter 56 | : codebook.config.autofilter; 57 | 58 | /********************* Group Settings *********************/ 59 | codebook.config.groups = codebook.config.groups || defaultSettings.groups; 60 | codebook.config.groups = codebook.config.groups.map(function(d) { 61 | if (typeof d == 'string') return { value_col: d }; 62 | else return d; 63 | }); 64 | 65 | /********************* Variable Label Settings *********************/ 66 | 67 | //check any user specified labels to make sure they are in the correct format 68 | codebook.config.variableLabels = codebook.config.variableLabels || []; 69 | codebook.config.variableLabels = codebook.config.variableLabels.filter( 70 | (label, i) => { 71 | const is_object = typeof label === 'object', 72 | has_value_col = label.hasOwnProperty('value_col'), 73 | has_label = label.hasOwnProperty('label'), 74 | legit = is_object && has_value_col && has_label; 75 | if (!legit) 76 | console.warn( 77 | `Item ${i} of settings.variableLabels (${JSON.stringify( 78 | label 79 | )}) must be an object with both a "value_col" and a "label" property.` 80 | ); 81 | 82 | return legit; 83 | } 84 | ); 85 | 86 | if ( 87 | codebook.config.variableLabels.length && 88 | defaultSettings.variableLabels.length 89 | ) { 90 | //merge the defaults with the user specified labels if both are populated 91 | var userLabelVars = codebook.config.variableLabels.map(m => m.value_col); 92 | 93 | //Keep the default label if the user hasn't specified a label for the column 94 | defaultSettings.variableLabels.forEach(function(defaultLabel) { 95 | if (userLabelVars.indexOf(defaultLabel.value_col) == -1) { 96 | codebook.config.variableLabels.push(defaultLabel); 97 | } 98 | }); 99 | } else { 100 | codebook.config.variableLabels = codebook.config.variableLabels.length 101 | ? codebook.config.variableLabels 102 | : defaultSettings.variableLabels; 103 | } 104 | //autogroups - don't use automatic groups if user specifies groups object 105 | codebook.config.autogroups = 106 | codebook.config.groups.length > 0 107 | ? false 108 | : codebook.config.autogroups == null 109 | ? defaultSettings.autogroups 110 | : codebook.config.autogroups; 111 | 112 | /********************* Variable Type Settings *********************/ 113 | 114 | //check any user specified types to make sure they are in the correct format 115 | codebook.config.variableTypes = codebook.config.variableTypes || []; 116 | codebook.config.variableTypes = codebook.config.variableTypes.filter( 117 | (type, i) => { 118 | const is_object = typeof type === 'object', 119 | has_value_col = type.hasOwnProperty('value_col'), 120 | has_type = type.hasOwnProperty('type'), 121 | legit_structure = is_object && has_value_col && has_type, 122 | legit = legit_structure 123 | ? ['continuous', 'categorical'].indexOf(type.type) > -1 124 | : false; 125 | if (!legit) 126 | console.warn( 127 | `Item ${i} of settings.variableType (${JSON.stringify( 128 | type 129 | )}) must be an object with both a "value_col" and a "type" property of "continuous" or "categorical".` 130 | ); 131 | 132 | return legit; 133 | } 134 | ); 135 | 136 | if ( 137 | codebook.config.variableTypes.length && 138 | defaultSettings.variableTypes.length 139 | ) { 140 | //merge the defaults with the user specified type if both are populated 141 | var userTypeVars = codebook.config.variableTypes.map(m => m.value_col); 142 | 143 | //Keep the default Type if the user hasn't specified a label for the column 144 | defaultSettings.variableTypes.forEach(function(defaultType) { 145 | if (userTypeVars.indexOf(defaultType.value_col) == -1) { 146 | codebook.config.variableTypes.push(defaultType); 147 | } 148 | }); 149 | } else { 150 | codebook.config.variableTypes = codebook.config.variableTypes.length 151 | ? codebook.config.variableTypes 152 | : defaultSettings.variableTypes; 153 | } 154 | 155 | /********************* Hidden Variable Settings ***************/ 156 | codebook.config.hiddenVariables = 157 | codebook.config.hiddenVariables || defaultSettings.hiddenVariables; 158 | codebook.config.hiddenVariables.push('web-codebook-index'); // internal variables should always be hidden 159 | 160 | /********************* Histogram Settings *********************/ 161 | codebook.config.nBins = codebook.config.nBins || defaultSettings.nBins; 162 | codebook.config.autobins = 163 | codebook.config.autobins == null 164 | ? defaultSettings.autobins 165 | : codebook.config.autobins; 166 | 167 | codebook.config.levelSplit = 168 | codebook.config.levelSplit || defaultSettings.levelSplit; 169 | 170 | codebook.config.maxLevels = 171 | codebook.config.maxLevels || defaultSettings.maxLevels; 172 | 173 | /********************* Nav Settings *********************/ 174 | codebook.config.tabs = codebook.config.tabs || defaultSettings.tabs; 175 | codebook.config.tabs = codebook.config.tabs.map(function(d) { 176 | if (typeof d == 'string') return { key: d }; 177 | else return d; 178 | }); 179 | 180 | codebook.config.defaultTab = 181 | codebook.config.defaultTab || codebook.config.tabs[0].key; 182 | if ( 183 | codebook.config.tabs.map(m => m.key).indexOf(codebook.config.defaultTab) == 184 | -1 185 | ) { 186 | console.warn( 187 | "Invalid starting tab of '" + 188 | codebook.config.defaultTab + 189 | "' specified. Using '" + 190 | codebook.config.tabs[0] + 191 | "' instead." 192 | ); 193 | codebook.config.defaultTab = codebook.config.tabs[0].key; 194 | } 195 | 196 | /********************* Missing Value Settings *********************/ 197 | codebook.config.whiteSpaceAsMissing = 198 | codebook.config.whiteSpaceAsMissing == undefined 199 | ? defaultSettings.whiteSpaceAsMissing 200 | : codebook.config.whiteSpaceAsMissing; 201 | 202 | codebook.config.missingValues = 203 | codebook.config.missingValues || defaultSettings.missingValues; 204 | 205 | /********************* Control Visibility Settings *********************/ 206 | codebook.config.controlVisibility = 207 | codebook.config.controlVisibility || defaultSettings.controlVisibility; 208 | 209 | /********************* Chart Visibility Settings *********************/ 210 | codebook.config.chartVisibility = 211 | codebook.config.chartVisibility || defaultSettings.chartVisibility; 212 | 213 | //hide the controls appropriately according to the start tab 214 | if (codebook.config.controlVisibility !== 'disabled') { 215 | var startTab = availableTabs.find(f => f.key == codebook.config.defaultTab); 216 | codebook.config.controlVisibility = startTab.controls 217 | ? codebook.config.controlVisibility 218 | : 'hidden'; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/createCodebook.js: -------------------------------------------------------------------------------- 1 | import { init } from './codebook/init'; 2 | import { layout } from './codebook/layout'; 3 | import { controls } from './codebook/controls'; 4 | import { nav } from './codebook/nav'; 5 | import { summaryTable } from './codebook/summaryTable'; 6 | import { dataListing } from './codebook/dataListing'; 7 | import { chartMaker } from './codebook/chartMaker'; 8 | import { util } from './codebook/util'; 9 | import { data } from './codebook/data'; 10 | import { settings } from './codebook/settings'; 11 | import { title } from './codebook/title'; 12 | import { instructions } from './codebook/instructions'; 13 | import clone from './util/clone'; 14 | 15 | export function createCodebook(element = 'body', config) { 16 | let codebook = { 17 | element: element, 18 | config: config, 19 | init: init, 20 | layout: layout, 21 | controls: controls, 22 | title: title, 23 | nav: nav, 24 | instructions: instructions, 25 | summaryTable: summaryTable, 26 | dataListing: dataListing, 27 | chartMaker: chartMaker, 28 | data: data, 29 | util: util, 30 | settings: settings 31 | }; 32 | 33 | var cbClone = clone(codebook); 34 | cbClone.events = { 35 | init() {}, 36 | complete() {} 37 | }; 38 | 39 | cbClone.on = function(event, callback) { 40 | let possible_events = ['init', 'complete']; 41 | if (possible_events.indexOf(event) < 0) { 42 | return; 43 | } 44 | if (callback) { 45 | cbClone.events[event] = callback; 46 | } 47 | }; 48 | 49 | return cbClone; 50 | } 51 | -------------------------------------------------------------------------------- /src/createExplorer.js: -------------------------------------------------------------------------------- 1 | import { init } from './explorer/init'; 2 | import { layout } from './explorer/layout'; 3 | import { fileListing } from './explorer/fileListing'; 4 | import { makeCodebook } from './explorer/makeCodebook'; 5 | import { addFile } from './explorer/addFile'; 6 | 7 | export function createExplorer(element = 'body', config) { 8 | let explorer = { 9 | element: element, 10 | config: config, 11 | init: init, 12 | layout: layout, 13 | fileListing: fileListing, 14 | makeCodebook: makeCodebook, 15 | addFile: addFile 16 | }; 17 | 18 | explorer.events = { 19 | init() {}, 20 | addFile() {}, 21 | makeCodebook() {} 22 | }; 23 | 24 | explorer.on = function(event, callback) { 25 | let possible_events = ['init', 'addFile', 'makeCodebook']; 26 | if (possible_events.indexOf(event) < 0) { 27 | return; 28 | } 29 | if (callback) { 30 | explorer.events[event] = callback; 31 | } 32 | }; 33 | 34 | return explorer; 35 | } 36 | -------------------------------------------------------------------------------- /src/explorer/addFile.js: -------------------------------------------------------------------------------- 1 | import { csv, merge } from 'd3'; 2 | 3 | export function addFile(label, csv_raw) { 4 | var explorer = this; 5 | 6 | // parse the file object 7 | this.newFileObject = {}; 8 | this.newFileObject[explorer.config.labelColumn] = label; 9 | this.newFileObject.json = csv.parse(csv_raw); 10 | this.newFileObject.settings = {}; 11 | this.newFileObject.fileID = explorer.config.files.length + 1; 12 | 13 | //call the addFile event (if any) 14 | explorer.events.addFile.call(this); 15 | 16 | //add new files to file list 17 | this.config.files = merge([[explorer.newFileObject], this.config.files]); 18 | 19 | //re-draw the file listing 20 | explorer.codebook.fileListing.table.draw(this.config.files); 21 | } 22 | -------------------------------------------------------------------------------- /src/explorer/defaultSettings.js: -------------------------------------------------------------------------------- 1 | const defaultSettings = { 2 | ignoredColumns: [], 3 | meta: [], 4 | defaultCodebookSettings: {}, 5 | tableConfig: { 6 | sortable: false, 7 | searchable: false, 8 | pagination: false, 9 | exportable: false 10 | }, 11 | fileLoader: false 12 | }; 13 | 14 | export default defaultSettings; 15 | -------------------------------------------------------------------------------- /src/explorer/fileListing.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Define controls object. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { init } from './fileListing/init'; 6 | 7 | export const fileListing = { 8 | init: init 9 | }; 10 | -------------------------------------------------------------------------------- /src/explorer/fileListing/init.js: -------------------------------------------------------------------------------- 1 | import { createTable } from 'webcharts'; 2 | import { onDraw } from './onDraw'; 3 | export function init() { 4 | var explorer = this; 5 | 6 | var fileWrap = explorer.codebook.fileListing.wrap; 7 | fileWrap.selectAll('*').remove(); //Clear controls. 8 | 9 | //Make file selector 10 | var file_select_wrap = fileWrap 11 | .append('div') 12 | .classed('listing-container', true); 13 | 14 | //Create the table 15 | explorer.codebook.fileListing.table = createTable( 16 | '.web-codebook .fileListing .listing-container', 17 | explorer.config.tableConfig 18 | ); 19 | 20 | //show the selected file first 21 | explorer.config.files.forEach(d => (d.selected = d == explorer.current)); 22 | var sortedFiles = explorer.config.files.sort(function(a, b) { 23 | return a.selected ? -1 : b.selected ? 1 : 0; 24 | }); 25 | 26 | //assign callbacks and initialize 27 | onDraw.call(explorer); 28 | explorer.codebook.fileListing.table.init(sortedFiles); 29 | } 30 | -------------------------------------------------------------------------------- /src/explorer/fileListing/onDraw.js: -------------------------------------------------------------------------------- 1 | import { select as d3select } from 'd3'; 2 | 3 | export function onDraw() { 4 | var explorer = this; 5 | 6 | explorer.codebook.fileListing.table.on('draw', function() { 7 | //highlight the current row 8 | this.table 9 | .select('tbody') 10 | .selectAll('tr') 11 | .classed('selected', f => f.fileID === explorer.current.fileID); 12 | 13 | //Linkify the labelColumn 14 | var labelCells = this.table 15 | .selectAll('tbody tr') 16 | .on('click', function(d) { 17 | explorer.current = d; 18 | explorer.current.event = 'click'; 19 | explorer.makeCodebook(explorer); 20 | }) 21 | .selectAll('td') 22 | .filter(f => f.col == explorer.config.labelColumn) 23 | .classed('link', true); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/explorer/init.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Initialize explorer 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | import { select as d3select } from 'd3'; 6 | import { setDefaults } from './setDefaults'; 7 | 8 | export function init() { 9 | var explorer = this; 10 | var settings = this.config; 11 | 12 | //call the init callback 13 | this.events.init.call(this); 14 | 15 | //set the defailts 16 | setDefaults.call(this); 17 | 18 | //prepare to draw the codebook for the first file 19 | this.current = this.config.files[0]; 20 | this.current.event = 'load'; 21 | 22 | //create wrapper in specified div 23 | this.wrap = d3select(this.element) 24 | .append('div') 25 | .attr('class', 'web-codebook-explorer'); 26 | 27 | //layout the divs 28 | this.layout.call(this); 29 | 30 | //draw first codebook 31 | this.makeCodebook.call(this); 32 | } 33 | -------------------------------------------------------------------------------- /src/explorer/initFileLoad.js: -------------------------------------------------------------------------------- 1 | import { time } from 'd3'; 2 | import { addFile } from './addFile'; 3 | 4 | export function initFileLoad() { 5 | //draw the control 6 | var explorer = this; 7 | explorer.dataFileLoad = {}; 8 | explorer.dataFileLoad.wrap = explorer.codebook.fileListing.wrap 9 | .insert('div', '*') 10 | .attr('class', 'dataLoader'); 11 | 12 | explorer.dataFileLoad.wrap.append('span').text('Add a local .csv file: '); 13 | 14 | explorer.dataFileLoad.loader_wrap = explorer.dataFileLoad.wrap 15 | .append('label') 16 | .attr('class', 'file-load-label'); 17 | 18 | explorer.dataFileLoad.loader_label = explorer.dataFileLoad.loader_wrap 19 | .append('span') 20 | .text('Choose a File'); 21 | 22 | explorer.dataFileLoad.loader_input = explorer.dataFileLoad.loader_wrap 23 | .append('input') 24 | .attr('type', 'file') 25 | .attr('class', 'file-load-input') 26 | .on('change', function() { 27 | var files = this.files; 28 | explorer.dataFileLoad.loader_label.text(files[0].name); 29 | 30 | if (this.value.slice(-4).toLowerCase() == '.csv') { 31 | loadStatus.text(' loading ...').style('color', 'green'); 32 | var fr = new FileReader(); 33 | fr.onload = function(e) { 34 | // get the current date/time 35 | var d = new Date(); 36 | var n = time.format('%X')(d); 37 | 38 | addFile.call(explorer, files[0].name, e.target.result); 39 | 40 | //clear the file input 41 | loadStatus.text('Loaded.').style('color', 'green'); 42 | explorer.dataFileLoad.loader_input.property('value', ''); 43 | }; 44 | 45 | fr.readAsText(files.item(0)); 46 | } else { 47 | loadStatus.text("Can't Load. File is not a csv.").style('color', 'red'); 48 | } 49 | }); 50 | 51 | var loadStatus = explorer.dataFileLoad.wrap 52 | .append('span') 53 | .attr('class', 'loadStatus') 54 | .text(''); 55 | 56 | loadStatus 57 | .append('sup') 58 | .html('ⓘ') 59 | .property( 60 | 'title', 61 | 'Create a codebook for a local file. File is added to the data set list, and is only available for a single session and is not saved.' 62 | ) 63 | .style('cursor', 'help'); 64 | } 65 | -------------------------------------------------------------------------------- /src/explorer/layout.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------------------------\ 2 | Generate HTML containers. 3 | \------------------------------------------------------------------------------------------------*/ 4 | 5 | export function layout() { 6 | this.codebookWrap = this.wrap.append('div').attr('class', 'codebookWrap'); 7 | } 8 | -------------------------------------------------------------------------------- /src/explorer/makeCodebook.js: -------------------------------------------------------------------------------- 1 | import { csv as d3csv } from 'd3'; 2 | import { merge as d3merge } from 'd3'; 3 | import { initFileLoad } from './initFileLoad'; 4 | 5 | export function makeCodebook() { 6 | var explorer = this; 7 | 8 | explorer.codebookWrap.selectAll('*').remove(); 9 | 10 | //add the Files section to the nav for each config 11 | this.current.settings.tabs = this.current.settings.tabs 12 | ? d3merge([['files'], this.current.settings.tabs]) 13 | : ['files', 'codebook', 'listing', 'chartMaker', 'settings']; 14 | 15 | //set the default tab to the codebook or listing view assuming they are visible 16 | if (this.current.event == 'click') { 17 | this.current.settings.defaultTab = 18 | this.current.settings.tabs 19 | .map(tab => (tab.key ? tab.key : tab)) 20 | .indexOf('codebook') > -1 21 | ? 'codebook' 22 | : this.current.settings.tabs.indexOf('listing') > -1 23 | ? 'listing' 24 | : 'files'; 25 | } 26 | 27 | this.current.settings.dataName = 28 | '"' + this.current[this.config.labelColumn] + '"'; 29 | 30 | //reset the group to null (only matters the 2nd time the file is clicked) 31 | delete this.current.settings.group; 32 | 33 | //pass along any relevant column metadata 34 | this.current.settings.meta = explorer.config.meta.filter( 35 | f => f.file == this.current[this.config.labelColumn] 36 | ); 37 | 38 | //create the codebook 39 | explorer.codebook = webcodebook.createCodebook( 40 | '.web-codebook-explorer .codebookWrap', 41 | this.current.settings 42 | ); 43 | 44 | explorer.codebook.on('complete', function() { 45 | explorer.fileListing.init.call(explorer); 46 | if (explorer.config.fileLoader) { 47 | initFileLoad.call(explorer); 48 | } 49 | }); 50 | 51 | if (this.current.json) { 52 | explorer.codebook.init(this.current.json); 53 | } else if (this.current.path) { 54 | d3csv(this.current.path, function(error, data) { 55 | explorer.codebook.init(data); 56 | }); 57 | } else { 58 | alert('No data provided for the selected file.'); 59 | } 60 | 61 | //call the makeCodebook event (if any) 62 | explorer.events.makeCodebook.call(this); 63 | } 64 | -------------------------------------------------------------------------------- /src/explorer/setDefaults.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from './defaultSettings'; 2 | 3 | export function setDefaults() { 4 | var explorer = this; 5 | /********************* meta *********************/ 6 | explorer.config.meta = explorer.config.meta || defaultSettings.meta; 7 | 8 | /********************* ignoredColumns *********************/ 9 | explorer.config.ignoredColumns = 10 | explorer.config.ignoredColumns || defaultSettings.ignoredColumns; 11 | 12 | /********************* labelColumn *********************/ 13 | var firstKey = Object.keys(explorer.config.files[0])[0]; 14 | explorer.config.labelColumn = explorer.config.labelColumn || firstKey; 15 | 16 | /********************* tableConfig ***************/ 17 | explorer.config.tableConfig = 18 | explorer.config.tableConfig || defaultSettings.tableConfig; 19 | 20 | //drop ignoredColumns and system variables 21 | explorer.config.tableConfig.cols = Object.keys(explorer.config.files[0]) 22 | .filter(f => explorer.config.ignoredColumns.indexOf(f) == -1) 23 | .filter( 24 | f => ['fileID', 'settings', 'selected', 'event', 'json'].indexOf(f) == -1 25 | ); //drop system variables from table 26 | 27 | /********************* defaultCodebookSettings ***************/ 28 | explorer.config.defaultCodebookSettings = 29 | explorer.config.defaultCodebookSettings || 30 | defaultSettings.defaultCodebookSettings; 31 | 32 | /********************* files[].settings ***************/ 33 | explorer.config.files.forEach(function(f, i) { 34 | f.settings = f.settings || explorer.config.defaultCodebookSettings; 35 | f.fileID = i; 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './polyfills/index'; 2 | import { createCodebook } from './createCodebook'; 3 | import { createExplorer } from './createExplorer'; 4 | import { charts } from './charts'; 5 | 6 | export default { 7 | createCodebook: createCodebook, 8 | createChart: createCodebook, 9 | createExplorer: createExplorer, 10 | charts: charts 11 | }; 12 | -------------------------------------------------------------------------------- /src/polyfills/array-find.js: -------------------------------------------------------------------------------- 1 | if (!Array.prototype.find) { 2 | Object.defineProperty(Array.prototype, 'find', { 3 | value: function(predicate) { 4 | // 1. Let O be ? ToObject(this value). 5 | if (this == null) { 6 | throw new TypeError('"this" is null or not defined'); 7 | } 8 | 9 | var o = Object(this); 10 | 11 | // 2. Let len be ? ToLength(? Get(O, 'length')). 12 | var len = o.length >>> 0; 13 | 14 | // 3. If IsCallable(predicate) is false, throw a TypeError exception. 15 | if (typeof predicate !== 'function') { 16 | throw new TypeError('predicate must be a function'); 17 | } 18 | 19 | // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. 20 | var thisArg = arguments[1]; 21 | 22 | // 5. Let k be 0. 23 | var k = 0; 24 | 25 | // 6. Repeat, while k < len 26 | while (k < len) { 27 | // a. Let Pk be ! ToString(k). 28 | // b. Let kValue be ? Get(O, Pk). 29 | // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). 30 | // d. If testResult is true, return kValue. 31 | var kValue = o[k]; 32 | if (predicate.call(thisArg, kValue, k, o)) { 33 | return kValue; 34 | } 35 | // e. Increase k by 1. 36 | k++; 37 | } 38 | 39 | // 7. Return undefined. 40 | return undefined; 41 | } 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/polyfills/array-findIndex.js: -------------------------------------------------------------------------------- 1 | if (!Array.prototype.findIndex) { 2 | Object.defineProperty(Array.prototype, 'findIndex', { 3 | value: function(predicate) { 4 | // 1. Let O be ? ToObject(this value). 5 | if (this == null) { 6 | throw new TypeError('"this" is null or not defined'); 7 | } 8 | 9 | var o = Object(this); 10 | 11 | // 2. Let len be ? ToLength(? Get(O, "length")). 12 | var len = o.length >>> 0; 13 | 14 | // 3. If IsCallable(predicate) is false, throw a TypeError exception. 15 | if (typeof predicate !== 'function') { 16 | throw new TypeError('predicate must be a function'); 17 | } 18 | 19 | // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. 20 | var thisArg = arguments[1]; 21 | 22 | // 5. Let k be 0. 23 | var k = 0; 24 | 25 | // 6. Repeat, while k < len 26 | while (k < len) { 27 | // a. Let Pk be ! ToString(k). 28 | // b. Let kValue be ? Get(O, Pk). 29 | // c. Let testResult be ToBoolean(? Call(predicate, T, � kValue, k, O �)). 30 | // d. If testResult is true, return k. 31 | var kValue = o[k]; 32 | if (predicate.call(thisArg, kValue, k, o)) { 33 | return k; 34 | } 35 | // e. Increase k by 1. 36 | k++; 37 | } 38 | 39 | // 7. Return -1. 40 | return -1; 41 | } 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/polyfills/index.js: -------------------------------------------------------------------------------- 1 | import './object-assign'; 2 | import './array-find'; 3 | import './array-findIndex'; 4 | -------------------------------------------------------------------------------- /src/polyfills/object-assign.js: -------------------------------------------------------------------------------- 1 | if (typeof Object.assign != 'function') { 2 | Object.defineProperty(Object, 'assign', { 3 | value: function assign(target, varArgs) { 4 | // .length of function is 2 5 | 'use strict'; 6 | 7 | if (target == null) { 8 | // TypeError if undefined or null 9 | throw new TypeError('Cannot convert undefined or null to object'); 10 | } 11 | 12 | var to = Object(target); 13 | 14 | for (var index = 1; index < arguments.length; index++) { 15 | var nextSource = arguments[index]; 16 | 17 | if (nextSource != null) { 18 | // Skip over if undefined or null 19 | for (var nextKey in nextSource) { 20 | // Avoid bugs when hasOwnProperty is shadowed 21 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { 22 | to[nextKey] = nextSource[nextKey]; 23 | } 24 | } 25 | } 26 | } 27 | 28 | return to; 29 | }, 30 | writable: true, 31 | configurable: true 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/util/clone.js: -------------------------------------------------------------------------------- 1 | export default function clone(obj) { 2 | let copy; 3 | 4 | //boolean, number, string, null, undefined 5 | if ('object' != typeof obj || null == obj) return obj; 6 | 7 | //date 8 | if (obj instanceof Date) { 9 | copy = new Date(); 10 | copy.setTime(obj.getTime()); 11 | return copy; 12 | } 13 | 14 | //array 15 | if (obj instanceof Array) { 16 | copy = []; 17 | for (var i = 0, len = obj.length; i < len; i++) { 18 | copy[i] = clone(obj[i]); 19 | } 20 | return copy; 21 | } 22 | 23 | //object 24 | if (obj instanceof Object) { 25 | copy = {}; 26 | for (var attr in obj) { 27 | if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]); 28 | } 29 | return copy; 30 | } 31 | 32 | throw new Error('Unable to copy [obj]! Its type is not supported.'); 33 | } 34 | -------------------------------------------------------------------------------- /test-page/default/index.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,300); 2 | * { 3 | padding: 0; 4 | margin: 0; 5 | font-family: 'Open Sans'; 6 | } 7 | #title { 8 | width: 96%; 9 | padding: 0 0 12px 0; 10 | border-bottom: 2px solid lightgray; 11 | margin: 24px 2% 12px 2%; 12 | font-size: 32px; 13 | font-weight: normal; 14 | } 15 | #subtitle { 16 | width: 96%; 17 | margin: 0 2% 12px 2%; 18 | font-size: 24px; 19 | font-weight: lighter; 20 | } 21 | #container { 22 | width: 96%; 23 | margin: 12px 2%; 24 | display: inline-block; 25 | } 26 | -------------------------------------------------------------------------------- /test-page/default/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Web Codebook 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Web Codebook
18 |
Test Page
19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test-page/default/index.js: -------------------------------------------------------------------------------- 1 | fetch( 2 | 'https://raw.githubusercontent.com/RhoInc/data-library/master/dataFiles.json' 3 | ) 4 | .then(response => response.json()) 5 | .then(json => { 6 | json.forEach(fileObj => { 7 | fileObj.github_url = `https://raw.githubusercontent.com/RhoInc/data-library/master/${fileObj.rel_path.replace( 8 | /^\.\//, 9 | '' 10 | )}`; 11 | fileObj.dataName = (/sdtm|adam/.test(fileObj.rel_path) 12 | ? fileObj.filename.toUpperCase() 13 | : fileObj.filename 14 | .split(/[_-]|(?=[A-Z])/) 15 | .map(str => { 16 | return /^ad(?!verse)/i.test(str) 17 | ? str.toUpperCase() 18 | : str.substring(0, 1).toUpperCase() + 19 | str.substring(1).toLowerCase(); 20 | }) 21 | .join(' ') 22 | ).replace(/\.csv/i, ''); 23 | }); 24 | console.log('Data file metadata:'); 25 | console.log(json); 26 | //const fileObj = json[Math.floor(Math.random() * json.length)]; 27 | const fileObj = json[8]; 28 | console.log('Select data file metadata:'); 29 | console.log(fileObj); 30 | d3.csv( 31 | fileObj.github_url, 32 | function(d) { 33 | return d; 34 | }, 35 | function(error, data) { 36 | if (error) { 37 | console.log(error); 38 | alert(`${fileObj.github_url} does not exist. Please reload page.`); 39 | } 40 | 41 | var instance = webcodebook.createCodebook('#container', { 42 | dataName: fileObj.dataName 43 | }); 44 | instance.init(data); 45 | } 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /test-page/explorer/explorer.js: -------------------------------------------------------------------------------- 1 | function cleanData() { 2 | var explorer = this; 3 | var config = this.config; 4 | 5 | config.files.forEach(function(f) { 6 | f.path = 7 | 'https://raw.githubusercontent.com/RhoInc/data-library/master' + 8 | f.rel_path.slice(1); 9 | f.shortname = f.filename.replace(/\.[^/.]+$/, ''); 10 | return f; 11 | }); 12 | } 13 | 14 | function initExplorer(fileList, settings) { 15 | settings.files = fileList; 16 | var explorer = webcodebook.createExplorer('#container', settings); 17 | explorer.on('init', cleanData); 18 | explorer.on('addFile', function() { 19 | console.log(this.newFileObject); 20 | }); 21 | explorer.on('makeCodebook', function() { 22 | console.log(this.current); 23 | }); 24 | explorer.init(); 25 | } 26 | 27 | var settings = { 28 | labelColumn: 'filename', 29 | ignoredColumns: ['local_path', 'rel_path', 'path'], 30 | fileLoader: true, 31 | metaFiles: ['ae', 'dm', 'lb'], 32 | defaultCodebookSettings: { 33 | autogroups: 2 34 | } 35 | }; 36 | 37 | document.onreadystatechange = function() { 38 | d3.json( 39 | 'https://raw.githubusercontent.com/RhoInc/data-library/master/dataFiles.json', 40 | function(error, dataFiles) { 41 | initExplorer(dataFiles, settings); 42 | 43 | d3.select('body') 44 | .append('p') 45 | .text('Settings:'); 46 | d3.select('body') 47 | .append('textarea') 48 | .property('rows', '10') 49 | .property('cols', '100') 50 | .property('value', JSON.stringify(settings)) 51 | .on('change', function() { 52 | delete explorer; 53 | d3.select('#container') 54 | .selectAll('*') 55 | .remove(); 56 | initExplorer(dataFiles, JSON.parse(this.value)); 57 | }); 58 | } 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /test-page/explorer/index.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,300); 2 | * { 3 | padding: 0; 4 | margin: 0; 5 | font-family: 'Open Sans'; 6 | } 7 | #title { 8 | width: 96%; 9 | padding: 0 0 12px 0; 10 | border-bottom: 2px solid lightgray; 11 | margin: 24px 2% 12px 2%; 12 | font-size: 32px; 13 | font-weight: normal; 14 | } 15 | #subtitle { 16 | width: 96%; 17 | margin: 0 2% 12px 2%; 18 | font-size: 24px; 19 | font-weight: lighter; 20 | } 21 | #container { 22 | width: 96%; 23 | margin: 12px 2%; 24 | display: inline-block; 25 | } 26 | -------------------------------------------------------------------------------- /test-page/explorer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Web Codebook 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Web Codebook - Explorer
18 |
Test Page
19 |
20 | 21 | 22 | 23 | 24 | --------------------------------------------------------------------------------