hpcc-systems / Visualization

HPCC JavaScript Framework
https://hpcc-systems.github.io/Visualization/
Other
93 stars 62 forks source link

Add custom max values for Heat chart #4212

Closed jfivekillerln closed 3 months ago

jfivekillerln commented 3 months ago

The Heat chart currently finds the max values in the data and uses it to color the chart.

Given this set of data, the max value is 0.005 therefor 0.005 is RED

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
mon 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005
tue 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005
wed 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005
thu 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005
fri 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005
sat 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005
sun 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005

which produces

Screenshot 2024-07-19 083856

The visualization is correct in the sense that everything is the same but is misleading because to our users it says all things occur at all times.

We use this chart to show what day and what times crimes occur we would like to be able to set the maximin value to 1.0 so we can calculate the percentage of crimes occurring per hour per day.

If I had one crime occur every day and every hour, I would have a count table:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
mon 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
tue 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
wed 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
thu 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
fri 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
sat 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
sun 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

If I, then turned the count table into a percentage table I would have (1/168=0.005)

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
mon 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005
tue 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005
wed 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005
thu 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005
fri 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005
sat 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005
sun 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.005

If the max value in the visualization was then set to 1, it would produce a chart like:

Screenshot 2024-07-19 094814

The chart shows all things are equal, but instead of coloring all times as red, it colors them with the same cool color.

GordonSmith commented 3 months ago

@jfivekillerln I have added that new "published property" and added an example of how you might use "Standard Deviations" rather than raw values. This should provide pleasing results for any data (the example below is using 2 standard deviations as being "exceptional"):

import { Heat } from "@hpcc-js/chart";
import * as df from "@hpcc-js/dataflow";

const data = [["Sat", 0, 0.18998719125426], ["Sat", 1, 0.21651075448881343], ["Sat", 2, 0.29360917730014136], ["Sat", 3, 0.1651116355055364], ["Sat", 4, 0.11885237728786953], ["Sat", 5, 0.09315256213264339], ["Sat", 6, 0.19081093933358728], ["Sat", 7, 0.26505462252549605], ["Sat", 8, 0.31304625721290896], ["Sat", 9, 0.18326323889972618], ["Sat", 10, 0.5427922043058862], ["Sat", 11, 0.33890867440986455], ["Sat", 12, 0.22287115322174472], ["Sat", 13, 0.2587162108611005], ["Sat", 14, 0.28441551468915144], ["Sat", 15, 0.19982154681583783], ["Sat", 16, 0.2930569439508717], ["Sat", 17, 0.47723750380299584], ["Sat", 18, 0.22207706211858186], ["Sat", 19, 0.33558402511639085], ["Sat", 20, 0.3690611266071652], ["Sat", 21, 0.5573599155287506], ["Sat", 22, 0.6831520252391093], ["Sat", 23, 0.5778902129422021], ["Fri", 0, 0.22511997013849297], ["Fri", 1, 0.12872968433216836], ["Fri", 2, 0.14983215685472429], ["Fri", 3, 0.2927501476457219], ["Fri", 4, 0.10428671137370601], ["Fri", 5, 0.15568583035698308], ["Fri", 6, 0.24599132277783603], ["Fri", 7, 0.2731898378837191], ["Fri", 8, 0.44567225462048016], ["Fri", 9, 0.20923559143936044], ["Fri", 10, 0.37126545805966676], ["Fri", 11, 0.25775900638903304], ["Fri", 12, 0.3814741051135274], ["Fri", 13, 0.34006938709768136], ["Fri", 14, 0.27593924410503684], ["Fri", 15, 0.4419477474759613], ["Fri", 16, 0.38965124930012096], ["Fri", 17, 0.3172503892478122], ["Fri", 18, 0.46400589048905894], ["Fri", 19, 0.2558543126612278], ["Fri", 20, 0.3590657029853837], ["Fri", 21, 0.15632447799886998], ["Fri", 22, 0.26420326277870526], ["Fri", 23, 0.297398622995917], ["Thur", 0, 0.18232751016901919], ["Thur", 1, 0.10522908735769126], ["Thur", 2, 0.2871142995201194], ["Thur", 3, 0.13264031456847822], ["Thur", 4, 0.18082727623683653], ["Thur", 5, 0.09944495435126643], ["Thur", 6, 0.11995531000488316], ["Thur", 7, 0.32298185555518627], ["Thur", 8, 0.2721533776994879], ["Thur", 9, 0.27001193948954205], ["Thur", 10, 0.4749544279655059], ["Thur", 11, 0.2692331882016368], ["Thur", 12, 0.4508438176709558], ["Thur", 13, 0.4492663733353105], ["Thur", 14, 0.3835056079807946], ["Thur", 15, 0.3464395010469424], ["Thur", 16, 0.34637609647721146], ["Thur", 17, 0.28212732557990894], ["Thur", 18, 0.19046579349029374], ["Thur", 19, 0.3223897386862471], ["Thur", 20, 0.2504833320124048], ["Thur", 21, 0.28639128289431637], ["Thur", 22, 0.4919882701545998], ["Thur", 23, 0.11120547938200998], ["Wed", 0, 0.26539414376986187], ["Wed", 1, 0.1915083896006279], ["Wed", 2, 0.1144094554621247], ["Wed", 3, 0.1915083896006279], ["Wed", 4, 0.1915083896006279], ["Wed", 5, 0.06301033647884767], ["Wed", 6, 0.18917162440973667], ["Wed", 7, 0.2034494131242346], ["Wed", 8, 0.355351933711545], ["Wed", 9, 0.3451218109163239], ["Wed", 10, 0.3226346644031917], ["Wed", 11, 0.20270338677554525], ["Wed", 12, 0.12560445263704206], ["Wed", 13, 0.2519610675488765], ["Wed", 14, 0.30086133062670817], ["Wed", 15, 0.24946221164343108], ["Wed", 16, 0.31534569552000696], ["Wed", 17, 0.4939159736052912], ["Wed", 18, 0.31544642697353115], ["Wed", 19, 0.24156016147712192], ["Wed", 20, 0.27236557847721654], ["Wed", 21, 0.3740930973387977], ["Wed", 22, 0.1085307269282787], ["Wed", 23, 0.13101787344141086], ["Tue", 0, 0.38368457249213195], ["Tue", 1, 0.11328658098527632], ["Tue", 2, 0.14541071077033996], ["Tue", 3, 0.06827291576651778], ["Tue", 4, 0.17107115373307188], ["Tue", 5, 0.11538864700272793], ["Tue", 6, 0.11217648968780919], ["Tue", 7, 0.13902321169712045], ["Tue", 8, 0.3094408893002232], ["Tue", 9, 0.40688502041473745], ["Tue", 10, 0.36313484464602097], ["Tue", 11, 0.4601351437724185], ["Tue", 12, 0.4095924978076847], ["Tue", 13, 0.22557709663316622], ["Tue", 14, 0.3283753345997203], ["Tue", 15, 0.2706332020074705], ["Tue", 16, 0.24600410595721722], ["Tue", 17, 0.5543993311840548], ["Tue", 18, 0.2738964920399142], ["Tue", 19, 0.2781803911141563], ["Tue", 20, 0.17569661936038083], ["Tue", 21, 0.48912841726342815], ["Tue", 22, 0.38632966796969875], ["Tue", 23, 0.18644420525695468], ["Mon", 0, 0.6359227793699938], ["Mon", 1, 0.5221023728137567], ["Mon", 2, 0.37279943958541595], ["Mon", 3, 0.34710013575736504], ["Mon", 4, 0.28285085353288725], ["Mon", 5, 0.25751868261666566], ["Mon", 6, 0.28428921687686476], ["Mon", 7, 0.17089474585761072], ["Mon", 8, 0.36545166807707746], ["Mon", 9, 0.21043056304791902], ["Mon", 10, 0.3696194958825379], ["Mon", 11, 0.3105586505053191], ["Mon", 12, 0.4402250862225449], ["Mon", 13, 0.2582289160630875], ["Mon", 14, 0.2996172716093256], ["Mon", 15, 0.32338989464103557], ["Mon", 16, 0.3249376820005164], ["Mon", 17, 0.22535620329345835], ["Mon", 18, 0.2782090254359703], ["Mon", 19, 0.14818108140584293], ["Mon", 20, 0.32807825351090025], ["Mon", 21, 0.5908569587793598], ["Mon", 22, 0.5080470114204925], ["Mon", 23, 0.2530276960364474], ["Sun", 0, 0.6469792068804184], ["Sun", 1, 0.4528553787783882], ["Sun", 2, 0.540662526620971], ["Sun", 3, 0.48754995027343223], ["Sun", 4, 0.2125643313502361], ["Sun", 5, 0.1999907961108455], ["Sun", 6, 0.19768419922329403], ["Sun", 7, 0.1666001089126883], ["Sun", 8, 0.39061458969829144], ["Sun", 9, 0.3091105719450119], ["Sun", 10, 0.3605096909282889], ["Sun", 11, 0.23504738724596624], ["Sun", 12, 0.2493108587995572], ["Sun", 13, 0.27608139305975626], ["Sun", 14, 0.301086314583818], ["Sun", 15, 0.4038845525503721], ["Sun", 16, 0.46976394580954595], ["Sun", 17, 0.5597125318620746], ["Sun", 18, 0.3975047233847814], ["Sun", 19, 0.19067492630497088], ["Sun", 20, 0.348541567064394], ["Sun", 21, 0.7699334507681412], ["Sun", 22, 0.606812411955852], ["Sun", 23, 1]];

//  Calculate the standard deviation of the weight column  ---
const extractWeight = df.map(row => row[2]);
const distribution = df.scalar(df.distribution())(extractWeight(data));

//  Convert the weight column to standard deviations  ---
const toStdDevs = df.map(r => {
    return [r[0], r[1], distribution.mean + (r[2] - distribution.mean) / (distribution.deviation || 1)];
});
const stdDevData = toStdDevs(data);

//  Normalize the standard deviations and calculate the new mean and deviation  ---
const range = df.scalar(df.extent())(extractWeight(stdDevData));
const denominator = (range[1] - range[0]) || 1;
const normalize = df.map(r => {
    return [r[0], r[1], (r[2] - range[0]) / denominator];
});
const normalizedStdDevData = normalize(toStdDevs(data));
const normalizedMean = (distribution.mean - range[0]) / denominator;
const normalizedDeviation = (distribution.deviation - range[0]) / denominator;

new Heat()
    .target("target")
    .columns(["Day", "Hour", "weight"])
    .paletteID("RdYlGn")
    .reversePalette(true)
    .xAxisType("ordinal")
    .yAxisType("time")
    .yAxisTypeTimePattern("%H")
    .yAxisTickFormat("%H:00")
    .yAxisTitle("Hour")
    .orientation("vertical")
    .maxWeight(normalizedMean + 2 * normalizedDeviation)
    .data([...normalizedStdDevData])
    .render()
    ;
jfivekillerln commented 3 months ago

@GordonSmith - I do like this approach. Originally, I was thinking we would set the max value "somewhere" under 100% so you could get hotspots, but this is better thanks!

GordonSmith commented 3 months ago

Also I found the following palette more representative of the data in question (Green/Yellow/Red):

    .paletteID("RdYlGn")
    .reversePalette(true)