Skip to content

Commit

Permalink
update ds and add canvas option for big charts
Browse files Browse the repository at this point in the history
  • Loading branch information
jsladerman committed Dec 19, 2024
1 parent e41aa46 commit 64e19f2
Show file tree
Hide file tree
Showing 5 changed files with 1,217 additions and 1,011 deletions.
2 changes: 1 addition & 1 deletion assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@nivo/radial-bar": "0.88.0",
"@nivo/tooltip": "0.88.0",
"@nivo/treemap": "0.88.0",
"@pluralsh/design-system": "4.3.0",
"@pluralsh/design-system": "4.5.0",
"@react-hooks-library/core": "0.6.0",
"@saas-ui/use-hotkeys": "1.1.3",
"@tanstack/react-table": "8.20.5",
Expand Down
2 changes: 2 additions & 0 deletions assets/src/components/cost-management/CostManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export function CostManagement() {
<CostManagementTreeMap
colorScheme="blue"
data={nodeCostByCluster(usages)}
dataSize={usages.length}
/>
</Card>
<Card
Expand All @@ -129,6 +130,7 @@ export function CostManagement() {
<CostManagementTreeMap
colorScheme="purple"
data={memoryCostByCluster(usages)}
dataSize={usages.length}
/>
</Card>
</Flex>
Expand Down
147 changes: 104 additions & 43 deletions assets/src/components/cost-management/CostManagementTreeMap.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ResponsiveTreeMapHtml } from '@nivo/treemap'
import { ResponsiveTreeMapCanvas, ResponsiveTreeMapHtml } from '@nivo/treemap'
import { EmptyState } from '@pluralsh/design-system'
import {
ClusterNamespaceUsageFragment,
Expand All @@ -9,57 +9,96 @@ import { ComponentProps } from 'react'
import styled from 'styled-components'

const PARENT_NODE_NAME = 'total'
const MIN_COST_PERCENTAGE = 0.02

type TreeMapData = {
name: string
amount?: Nullable<number>
children?: Nullable<TreeMapData[]>
}

type TreeMapHtmlProps = Omit<
ComponentProps<typeof ResponsiveTreeMapHtml>,
'data'
>
type TreeMapCanvasProps = Omit<
ComponentProps<typeof ResponsiveTreeMapCanvas>,
'data'
>

type TreeMapProps = {
data: TreeMapData
dataSize?: number
type?: 'html' | 'canvas'
colorScheme?: 'blue' | 'purple'
} & (TreeMapHtmlProps | TreeMapCanvasProps)

const commonTreeMapProps = {
identity: 'name',
value: 'amount',
valueFormat: ' >-$.2',
innerPadding: 4,
outerPadding: 8,
label: (e) => e.id,
labelSkipSize: 16,
borderWidth: 0,
nodeOpacity: 0.9,
}

export function CostManagementTreeMap({
data,
type,
dataSize,
colorScheme = 'blue',
...props
}: { data: TreeMapData; colorScheme?: 'blue' | 'purple' } & Omit<
ComponentProps<typeof ResponsiveTreeMapHtml>,
'data'
>) {
}: TreeMapProps) {
if (isEmpty(data.children))
return <EmptyState message="No costs to display" />
const derivedType =
!dataSize || dataSize > 35 || type === 'canvas' ? 'canvas' : 'html'
return (
<WrapperSC>
<ResponsiveTreeMapHtml
data={data}
identity="name"
value="amount"
valueFormat=" >-$.2"
innerPadding={4}
outerPadding={8}
// label={(e) => e.id + ' (' + e.formattedValue + ')'}
label={(e) => e.id}
labelSkipSize={16}
labelTextColor={{
from: 'color',
modifiers: [['darker', 4]],
}}
parentLabelTextColor={{
from: 'color',
modifiers: [['darker', 4]],
}}
borderWidth={0}
colors={colorScheme === 'blue' ? blueScheme : purpleScheme}
nodeOpacity={0.8}
borderColor={{
from: 'color',
modifiers: [['darker', 1.2]],
}}
{...props}
/>
{derivedType === 'canvas' ? (
<ResponsiveTreeMapCanvas
data={data}
leavesOnly
colors={colorScheme === 'blue' ? blueScheme : purpleScheme}
labelTextColor={{
from: 'color',
modifiers: [['darker', 4]],
}}
borderColor={{
from: 'color',
modifiers: [['darker', 1.2]],
}}
{...commonTreeMapProps}
{...(props as TreeMapCanvasProps)}
/>
) : (
<ResponsiveTreeMapHtml
data={data}
colors={colorScheme === 'blue' ? blueScheme : purpleScheme}
labelTextColor={{
from: 'color',
modifiers: [['darker', 4]],
}}
parentLabelTextColor={{
from: 'color',
modifiers: [['darker', 4]],
}}
borderColor={{
from: 'color',
modifiers: [['darker', 1.2]],
}}
{...commonTreeMapProps}
{...(props as TreeMapHtmlProps)}
/>
)}
</WrapperSC>
)
}

// just used to override nivo styles
// just used to override nivo styles when rendered in html
const WrapperSC = styled.div(({ theme }) => ({
display: 'contents',
// tooltip text
Expand All @@ -71,19 +110,25 @@ const WrapperSC = styled.div(({ theme }) => ({
}))

export function nodeCostByCluster(usages: ClusterUsageTinyFragment[]) {
const avg =
usages.reduce((acc, usage) => acc + (usage.nodeCost ?? 0), 0) /
usages.length
const projectMap: Record<string, TreeMapData> = {}

for (const usage of usages) {
if (!usage.nodeCost || !usage.cluster?.project) continue

const project = usage.cluster.project.name
if (!projectMap[project])
projectMap[project] = { name: project, children: [] }

projectMap[project].children?.push({
name: usage.cluster?.name ?? usage.id,
amount: usage.nodeCost,
})
if (usage.nodeCost / avg > MIN_COST_PERCENTAGE)
projectMap[project].children?.push({
name: usage.cluster?.name ?? usage.id,
amount: usage.nodeCost,
})
}

return {
name: PARENT_NODE_NAME,
children: Object.values(projectMap),
Expand All @@ -92,29 +137,39 @@ export function nodeCostByCluster(usages: ClusterUsageTinyFragment[]) {

export function memoryCostByCluster(usages: ClusterUsageTinyFragment[]) {
const projectMap: Record<string, TreeMapData> = {}
const avg =
usages.reduce((acc, usage) => acc + (usage.memoryCost ?? 0), 0) /
usages.length

for (const usage of usages) {
if (!usage.memoryCost || !usage.cluster?.project) continue

const project = usage.cluster.project.name
if (!projectMap[project])
projectMap[project] = { name: project, children: [] }

projectMap[project].children?.push({
name: usage.cluster?.name ?? usage.id,
amount: usage.memoryCost,
})
if (usage.memoryCost / avg > MIN_COST_PERCENTAGE)
projectMap[project].children?.push({
name: usage.cluster?.name ?? usage.id,
amount: usage.memoryCost,
})
}

return {
name: PARENT_NODE_NAME,
children: Object.values(projectMap),
}
}

export function cpuCostByNamespace(usages: ClusterNamespaceUsageFragment[]) {
const avg =
usages.reduce((acc, usage) => acc + (usage.cpuCost ?? 0), 0) / usages.length
return {
name: PARENT_NODE_NAME,
children: usages
.filter((usage) => !!usage.cpuCost)
.filter(
(usage) => !!usage.cpuCost && usage.cpuCost / avg > MIN_COST_PERCENTAGE
)
.map((usage) => ({
name: usage.namespace ?? usage.id,
amount: usage.cpuCost,
Expand All @@ -123,10 +178,16 @@ export function cpuCostByNamespace(usages: ClusterNamespaceUsageFragment[]) {
}

export function memoryCostByNamespace(usages: ClusterNamespaceUsageFragment[]) {
const avg =
usages.reduce((acc, usage) => acc + (usage.memoryCost ?? 0), 0) /
usages.length
return {
name: PARENT_NODE_NAME,
children: usages
.filter((usage) => !!usage.memoryCost)
.filter(
(usage) =>
!!usage.memoryCost && usage.memoryCost / avg > MIN_COST_PERCENTAGE
)
.map((usage) => ({
name: usage.namespace ?? usage.id,
amount: usage.memoryCost,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function CostManagementDetailsNamespaces() {
.filter((node): node is ClusterNamespaceUsageFragment => !!node) || [],
[data?.clusterUsage?.namespaces?.edges]
)

return (
<Flex
direction="column"
Expand All @@ -75,8 +76,8 @@ export function CostManagementDetailsNamespaces() {
<CostManagementTreeMap
enableParentLabel={false}
colorScheme="blue"
nodeOpacity={0.9}
data={cpuCostByNamespace(usages)}
dataSize={usages.length}
/>
</Card>
<Card
Expand All @@ -98,6 +99,7 @@ export function CostManagementDetailsNamespaces() {
enableParentLabel={false}
colorScheme="purple"
data={memoryCostByNamespace(usages)}
dataSize={usages.length}
/>
</Card>
</Flex>
Expand Down
Loading

0 comments on commit 64e19f2

Please sign in to comment.