palantir-foundryfoundry-code-repositoriesfoundry-workshopfoundry-functions

How do I create a cumulative sum graph in Palantir Foundry Workshop?


I have some objects that represent maintenance jobs, each with a time column like this:

Due On
2021-12-01
2022-06-17
2022-07-05
2022-07-05
2022-08-01
2023-09-02

How can I generate a cumulative graph in Palantir Foundry Workshop, plotting values like this?

Cumulative graph


Solution

  • You can do this using a Foundry Function. Create a TypeScript functions repository and use the following code (see comments inline for an explanation):

    import { Function, Double, ThreeDimensionalAggregation, IRange, IRangeable, Timestamp, BucketKey, BucketValue } from "@foundry/functions-api";
    
    // Replace MaintenanceJob with your object
    // To make your object available, add it in the Settings > Ontology tab
    import { ObjectSet, MaintenanceJob } from "@foundry/ontology-api";
    
    export class MyFunctions {
        // You will find this function in Workshop after it's published
        // Replace MaintenanceJob with your object
        @Function()
        public async cumulativeJobsByMonth(jobs: ObjectSet<MaintenanceJob>): Promise<TwoDimensionalAggregation<IRange<Timestamp>, Double>> {
            const bucketedJobs = await jobs
                .groupBy(j => j.dueOn.byMonth())
                .count();
    
            const sortedBucketedJobs = sortBuckets(bucketedJobs);
            const cumulativeSortedBucketedJobs = cumulativeSum2D(sortedBucketedJobs);
    
            return cumulativeSortedBucketedJobs
        }
    }
    
    /**
     * Sort buckets of a 2D or 3D aggregation by the first axis in ascending order
     * 
     * Example input 1:
     * { buckets: [
     *   { key: { min: "2022-01-01", max: "2022-12-31" }, value: 456 },
     *   { key: { min: "2021-01-01", max: "2021-12-31" }, value: 123 },
     *   { key: { min: "2023-01-01", max: "2023-12-31" }, value: 789 },
     * ]}
     * 
     * Example output 1:
     * { buckets: [
     *   { key: { min: "2021-01-01", max: "2021-12-31" }, value: 123 },
     *   { key: { min: "2022-01-01", max: "2022-12-31" }, value: 456 },
     *   { key: { min: "2023-01-01", max: "2023-12-31" }, value: 789 },
     * ]}
     * 
     * Example input 2:
     * { buckets: [
     *   { key: 22, value: 456 },
     *   { key: 21, value: 123 },
     *   { key: 23, value: 789 },
     * ]}
     * 
     * Example output 2:
     * { buckets: [
     *   { key: 21, value: 123 },
     *   { key: 22, value: 456 },
     *   { key: 23, value: 789 },
     * ]}
     * 
     * Example input 3:
     * { buckets: [
     *   { key: { min: "2022-01-01", max: "2022-12-31" }, value: [{ key: "open", value: 789 }, { key: "closed", value: 910 }] },
     *   { key: { min: "2021-01-01", max: "2021-12-31" }, value: [{ key: "open", value: 123 }, { key: "closed", value: 456 }] },
     *   { key: { min: "2023-01-01", max: "2023-12-31" }, value: [{ key: "open", value: 314 }, { key: "closed", value: 42 }] },
     * ]}
     * 
     * Example output 3:
     * { buckets: [
     *   { key: { min: "2021-01-01", max: "2021-12-31" }, value: [{ key: "open", value: 123 }, { key: "closed", value: 456 }] },
     *   { key: { min: "2022-01-01", max: "2022-12-31" }, value: [{ key: "open", value: 789 }, { key: "closed", value: 910 }] },
     *   { key: { min: "2023-01-01", max: "2023-12-31" }, value: [{ key: "open", value: 314 }, { key: "closed", value: 42 }] },
     * ]}
     */
    function sortBuckets<K1 extends BucketKey, K2 extends BucketKey, V extends BucketValue>(buckets: ThreeDimensionalAggregation<K1, K2, V>): ThreeDimensionalAggregation<K1, K2, V>;
    function sortBuckets<K extends BucketKey, V extends BucketValue>(buckets: TwoDimensionalAggregation<K, V>): TwoDimensionalAggregation<K, V>;
    function sortBuckets<K extends BucketKey, V>(buckets: { buckets: { key: K, value: V}[] }): { buckets: { key: K, value: V}[] } {
        return {
            // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
            buckets: buckets.buckets.sort(({ key: k1 }, { key: k2 }) => {
                if (typeof k1 !== typeof k2) throw new Error("Inconsistent bucket key types")
    
                // If not objects, these must be either numbers or booleans which can be compared like this
                if (typeof k1 !== "object" || typeof k2 !== "object") return Number(k1) - Number(k2);
    
                // If a bucket doesn't have a minimum, it suggests that it is the global unbounded minimum bucket, so must be lower
                if (!(k1 as IRange<IRangeable>).min) return -1;
                if (!(k2 as IRange<IRangeable>).min) return 1;
    
                // Otherwise, compare both buckets' minimums
                return (k1 as IRange<IRangeable>).min!.valueOf() - (k2 as IRange<IRangeable>).min!.valueOf();
            }),
        };
    }
    
    /**
     * Calculates a cumulative sum for a TwoDimensionalAggregation over numbers, along the first axis
     * The order of the buckets into the function matters for how the values are aggregated
     * 
     * Example input 1:
     * { buckets: [
     *   { key: { min: "2021-01-01", max: "2021-12-31" }, value: 123 },
     *   { key: { min: "2022-01-01", max: "2022-12-31" }, value: 789 },
     *   { key: { min: "2023-01-01", max: "2023-12-31" }, value: 314 },
     * ]}
     * 
     * Example output 1:
     * { buckets: [
     *   { key: { min: "2021-01-01", max: "2021-12-31" }, value: 123 },
     *   { key: { min: "2022-01-01", max: "2022-12-31" }, value: 912 },
     *   { key: { min: "2023-01-01", max: "2023-12-31" }, value: 1226 },
     * ]}
     * 
     * Example input 2:
     * { buckets: [
     *   { key: 23, value: 314 },
     *   { key: 22, value: 789 },
     *   { key: 21, value: 123 },
     * ]}
     * 
     * Example output 2:
     * { buckets: [
     *   { key: 23, value: 314 },
     *   { key: 22, value: 1103 },
     *   { key: 21, value: 1226 },
     * ]}
     */
    const cumulativeSum2D = <K extends BucketKey>(buckets: TwoDimensionalAggregation<K, number>): TwoDimensionalAggregation<K, number> => {
        // This holds the running total
        let cumulativeValue = 0;
        
        return {
            buckets: buckets.buckets.map(b => ({
                key: b.key,
                // This line adds the current value to the running total, and uses the result of that
                value: cumulativeValue += b.value,
            }))
        }
    }
    

    Commit your changes and publish a function version. There is a step-by-step guide in the Palantir Foundry documentation on how to create a repository and publish functions.

    In your Workshop, you can create a 'Chart: XY' widget. As the data source, select the function you created and pass in the relevant object set. There's also Palantir Foundry documentation on using a derived aggregation in Workshop.