2019-02-23 01:41:19 +01:00
/ * *
* @ author tlwr [ toby @ toby . codes ]
2019-03-14 14:37:11 +01:00
* @ author Matt C [ me @ mitt . dev ]
2019-02-23 01:41:19 +01:00
* @ copyright Crown Copyright 2019
* @ license Apache - 2.0
* /
2019-03-14 14:37:11 +01:00
import * as d3temp from "d3" ;
import * as d3hexbintemp from "d3-hexbin" ;
import * as nodomtemp from "nodom" ;
2019-02-23 01:41:19 +01:00
import { getScatterValues , RECORD _DELIMITER _OPTIONS , COLOURS , FIELD _DELIMITER _OPTIONS } from "../lib/Charts" ;
import Operation from "../Operation" ;
import Utils from "../Utils" ;
2019-03-14 14:37:11 +01:00
const d3 = d3temp . default ? d3temp . default : d3temp ;
const d3hexbin = d3hexbintemp . default ? d3hexbintemp . default : d3hexbintemp ;
const nodom = nodomtemp . default ? nodomtemp . default : nodomtemp ;
2019-02-23 01:41:19 +01:00
/ * *
* Hex Density chart operation
* /
class HexDensityChart extends Operation {
/ * *
* HexDensityChart constructor
* /
constructor ( ) {
super ( ) ;
this . name = "Hex Density chart" ;
this . module = "Charts" ;
2019-03-10 14:08:47 +01:00
this . description = "Hex density charts are used in a similar way to scatter charts, however rather than rendering tens of thousands of points, it groups the points into a few hundred hexagons to show the distribution." ;
2019-02-23 01:41:19 +01:00
this . inputType = "string" ;
this . outputType = "html" ;
this . args = [
{
name : "Record delimiter" ,
type : "option" ,
value : RECORD _DELIMITER _OPTIONS ,
} ,
{
name : "Field delimiter" ,
type : "option" ,
value : FIELD _DELIMITER _OPTIONS ,
} ,
{
name : "Pack radius" ,
type : "number" ,
value : 25 ,
} ,
{
name : "Draw radius" ,
type : "number" ,
value : 15 ,
} ,
{
name : "Use column headers as labels" ,
type : "boolean" ,
value : true ,
} ,
{
name : "X label" ,
type : "string" ,
value : "" ,
} ,
{
name : "Y label" ,
type : "string" ,
value : "" ,
} ,
{
name : "Draw hexagon edges" ,
type : "boolean" ,
value : false ,
} ,
{
name : "Min colour value" ,
type : "string" ,
value : COLOURS . min ,
} ,
{
name : "Max colour value" ,
type : "string" ,
value : COLOURS . max ,
} ,
{
name : "Draw empty hexagons within data boundaries" ,
type : "boolean" ,
value : false ,
}
] ;
}
/ * *
2019-03-10 14:08:47 +01:00
* Hex Bin chart operation .
*
2019-02-23 01:41:19 +01:00
* @ param { string } input
* @ param { Object [ ] } args
* @ returns { html }
* /
run ( input , args ) {
2019-03-10 12:44:02 +01:00
const recordDelimiter = Utils . charRep ( args [ 0 ] ) ,
fieldDelimiter = Utils . charRep ( args [ 1 ] ) ,
2019-02-23 01:41:19 +01:00
packRadius = args [ 2 ] ,
drawRadius = args [ 3 ] ,
columnHeadingsAreIncluded = args [ 4 ] ,
drawEdges = args [ 7 ] ,
minColour = args [ 8 ] ,
maxColour = args [ 9 ] ,
drawEmptyHexagons = args [ 10 ] ,
dimension = 500 ;
let xLabel = args [ 5 ] ,
yLabel = args [ 6 ] ;
const { headings , values } = getScatterValues (
input ,
recordDelimiter ,
fieldDelimiter ,
columnHeadingsAreIncluded
) ;
if ( headings ) {
xLabel = headings . x ;
yLabel = headings . y ;
}
2019-03-10 13:03:53 +01:00
const document = new nodom . Document ( ) ;
2019-02-23 01:41:19 +01:00
let svg = document . createElement ( "svg" ) ;
svg = d3 . select ( svg )
. attr ( "width" , "100%" )
. attr ( "height" , "100%" )
. attr ( "viewBox" , ` 0 0 ${ dimension } ${ dimension } ` ) ;
const margin = {
top : 10 ,
right : 0 ,
bottom : 40 ,
left : 30 ,
} ,
width = dimension - margin . left - margin . right ,
height = dimension - margin . top - margin . bottom ,
marginedSpace = svg . append ( "g" )
. attr ( "transform" , "translate(" + margin . left + "," + margin . top + ")" ) ;
const hexbin = d3hexbin . hexbin ( )
. radius ( packRadius )
. extent ( [ 0 , 0 ] , [ width , height ] ) ;
const hexPoints = hexbin ( values ) ,
maxCount = Math . max ( ... hexPoints . map ( b => b . length ) ) ;
const xExtent = d3 . extent ( hexPoints , d => d . x ) ,
yExtent = d3 . extent ( hexPoints , d => d . y ) ;
xExtent [ 0 ] -= 2 * packRadius ;
xExtent [ 1 ] += 3 * packRadius ;
yExtent [ 0 ] -= 2 * packRadius ;
yExtent [ 1 ] += 2 * packRadius ;
const xAxis = d3 . scaleLinear ( )
. domain ( xExtent )
. range ( [ 0 , width ] ) ;
const yAxis = d3 . scaleLinear ( )
. domain ( yExtent )
. range ( [ height , 0 ] ) ;
const colour = d3 . scaleSequential ( d3 . interpolateLab ( minColour , maxColour ) )
. domain ( [ 0 , maxCount ] ) ;
marginedSpace . append ( "clipPath" )
. attr ( "id" , "clip" )
. append ( "rect" )
. attr ( "width" , width )
. attr ( "height" , height ) ;
if ( drawEmptyHexagons ) {
marginedSpace . append ( "g" )
. attr ( "class" , "empty-hexagon" )
. selectAll ( "path" )
. data ( this . getEmptyHexagons ( hexPoints , packRadius ) )
. enter ( )
. append ( "path" )
. attr ( "d" , d => {
return ` M ${ xAxis ( d . x ) } , ${ yAxis ( d . y ) } ${ hexbin . hexagon ( drawRadius ) } ` ;
} )
. attr ( "fill" , ( d ) => colour ( 0 ) )
. attr ( "stroke" , drawEdges ? "black" : "none" )
. attr ( "stroke-width" , drawEdges ? "0.5" : "none" )
. append ( "title" )
. text ( d => {
const count = 0 ,
perc = 0 ,
tooltip = ` Count: ${ count } \n
Percentage : $ { perc . toFixed ( 2 ) } % \ n
Center : $ { d . x . toFixed ( 2 ) } , $ { d . y . toFixed ( 2 ) } \ n
` .replace(/ \s {2,}/g, " \n ");
return tooltip ;
} ) ;
}
marginedSpace . append ( "g" )
. attr ( "class" , "hexagon" )
. attr ( "clip-path" , "url(#clip)" )
. selectAll ( "path" )
. data ( hexPoints )
. enter ( )
. append ( "path" )
. attr ( "d" , d => {
return ` M ${ xAxis ( d . x ) } , ${ yAxis ( d . y ) } ${ hexbin . hexagon ( drawRadius ) } ` ;
} )
. attr ( "fill" , ( d ) => colour ( d . length ) )
. attr ( "stroke" , drawEdges ? "black" : "none" )
. attr ( "stroke-width" , drawEdges ? "0.5" : "none" )
. append ( "title" )
. text ( d => {
const count = d . length ,
perc = 100.0 * d . length / values . length ,
CX = d . x ,
CY = d . y ,
xMin = Math . min ( ... d . map ( d => d [ 0 ] ) ) ,
xMax = Math . max ( ... d . map ( d => d [ 0 ] ) ) ,
yMin = Math . min ( ... d . map ( d => d [ 1 ] ) ) ,
yMax = Math . max ( ... d . map ( d => d [ 1 ] ) ) ,
tooltip = ` Count: ${ count } \n
Percentage : $ { perc . toFixed ( 2 ) } % \ n
Center : $ { CX . toFixed ( 2 ) } , $ { CY . toFixed ( 2 ) } \ n
Min X : $ { xMin . toFixed ( 2 ) } \ n
Max X : $ { xMax . toFixed ( 2 ) } \ n
Min Y : $ { yMin . toFixed ( 2 ) } \ n
Max Y : $ { yMax . toFixed ( 2 ) }
` .replace(/ \s {2,}/g, " \n ");
return tooltip ;
} ) ;
marginedSpace . append ( "g" )
. attr ( "class" , "axis axis--y" )
. call ( d3 . axisLeft ( yAxis ) . tickSizeOuter ( - width ) ) ;
svg . append ( "text" )
. attr ( "transform" , "rotate(-90)" )
. attr ( "y" , - margin . left )
. attr ( "x" , - ( height / 2 ) )
. attr ( "dy" , "1em" )
. style ( "text-anchor" , "middle" )
. text ( yLabel ) ;
marginedSpace . append ( "g" )
. attr ( "class" , "axis axis--x" )
. attr ( "transform" , "translate(0," + height + ")" )
. call ( d3 . axisBottom ( xAxis ) . tickSizeOuter ( - height ) ) ;
svg . append ( "text" )
. attr ( "x" , width / 2 )
. attr ( "y" , dimension )
. style ( "text-anchor" , "middle" )
. text ( xLabel ) ;
return svg . _groups [ 0 ] [ 0 ] . outerHTML ;
}
/ * *
* Hex Bin chart operation .
*
* @ param { Object [ ] } - centres
* @ param { number } - radius
* @ returns { Object [ ] }
* /
getEmptyHexagons ( centres , radius ) {
const emptyCentres = [ ] ,
boundingRect = [ d3 . extent ( centres , d => d . x ) , d3 . extent ( centres , d => d . y ) ] ,
hexagonCenterToEdge = Math . cos ( 2 * Math . PI / 12 ) * radius ,
hexagonEdgeLength = Math . sin ( 2 * Math . PI / 12 ) * radius ;
let indent = false ;
for ( let y = boundingRect [ 1 ] [ 0 ] ; y <= boundingRect [ 1 ] [ 1 ] + radius ; y += hexagonEdgeLength + radius ) {
for ( let x = boundingRect [ 0 ] [ 0 ] ; x <= boundingRect [ 0 ] [ 1 ] + radius ; x += 2 * hexagonCenterToEdge ) {
let cx = x ;
const cy = y ;
if ( indent && x >= boundingRect [ 0 ] [ 1 ] ) break ;
if ( indent ) cx += hexagonCenterToEdge ;
emptyCentres . push ( { x : cx , y : cy } ) ;
}
indent = ! indent ;
}
return emptyCentres ;
}
}
export default HexDensityChart ;