Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit e679517

Browse files
committed
feat(plugins/plugin-client-common): grammy should assign bar colors based on categories
Fixes #7165
1 parent 8758f8d commit e679517

File tree

5 files changed

+93
-22
lines changed

5 files changed

+93
-22
lines changed

package-lock.json

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/src/webapp/models/table.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ export class Table<RowType extends Row = Row> {
175175
/** Column index to be interpreted as a complete timestamp column */
176176
completeColumnIdx?: number
177177

178-
/** Coloring strategy for e.g. 'grid' and 'sequence-diagram' */
179-
colorBy?: 'status' | 'duration'
178+
/** Coloring strategy for e.g. 'grid' and 'sequence-diagram' and 'histogram' */
179+
colorBy?: 'status' | 'duration' | 'default'
180180

181181
style?: TableStyle
182182

plugins/plugin-client-common/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"react-markdown": "5.0.3",
3131
"remark-gfm": "1.0.0",
3232
"spinkit": "2.0.1",
33+
"string-similarity-coloring": "1.0.4",
3334
"tmp": "0.2.1",
3435
"turndown": "7.0.0",
3536
"turndown-plugin-gfm": "1.0.2"

plugins/plugin-client-common/src/components/Content/Table/Histogram.tsx

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
*/
1616

1717
import React from 'react'
18-
import { Chart, ChartAxis, ChartBar, ChartLabel, ChartVoronoiContainer } from '@patternfly/react-charts'
18+
import stringColoring from 'string-similarity-coloring'
1919
import { REPL, Row, Tab, Table } from '@kui-shell/core'
20+
import { Chart, ChartAxis, ChartBar, ChartLabel, ChartVoronoiContainer } from '@patternfly/react-charts'
2021

2122
interface Props {
2223
response: Table
@@ -31,6 +32,7 @@ interface State {
3132
rows: Row[]
3233
animate: boolean
3334
counts: number[]
35+
colors: string[]
3436
scale: 'linear' | 'log'
3537
}
3638

@@ -62,24 +64,25 @@ function sameState(A: State, B: State): boolean {
6264
export default class Histogram extends React.PureComponent<Props, State> {
6365
private readonly horizontal = true
6466
private readonly barHeight = 10
65-
private readonly axisLabelFontSize = 9
67+
private readonly barSpacing = 0.1 // 10% of barHeight spacing between bars
68+
private readonly axisLabelFontSize = 0.9 * this.barHeight
6669
private readonly minAxisLabelChars = 4
6770
private readonly maxAxisLabelChars = 13
68-
private readonly barLabelFontSize = 6
71+
private readonly barLabelFontSize = 0.65 * this.barHeight
6972
private readonly minBarLabelChars = 1
7073
private readonly maxBarLabelChars = 6
7174

7275
public constructor(props: Props) {
7376
super(props)
74-
this.state = Histogram.filterRows(props.response.body)
77+
this.state = Histogram.filterRows(props.response.body, props.response.colorBy !== undefined)
7578
}
7679

7780
public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
7881
console.error(error, errorInfo)
7982
}
8083

8184
public static getDerivedStateFromProps(props: Props, state: State) {
82-
const newState = Histogram.filterRows(props.response.body)
85+
const newState = Histogram.filterRows(props.response.body, props.response.colorBy !== undefined)
8386

8487
// to avoid re-renders when nothing has changed...
8588
if (state && sameState(state, newState)) {
@@ -94,17 +97,42 @@ export default class Histogram extends React.PureComponent<Props, State> {
9497
}
9598
}
9699

97-
private static filterRows(rows: Props['response']['body']): State {
100+
private static filterRows(rows: Props['response']['body'], useFancyColoring: boolean): State {
98101
const countOf = (row: Row) => parseInt(row.attributes.find(_ => _.key && /^count$/i.test(_.key)).value, 10)
99102
const counts = rows.map(countOf)
100103
const yMax = counts.reduce((yMax, count) => Math.max(yMax, count), 0)
101-
const filteredRows = rows.filter((row, ridx) => (!Histogram.isTiny(counts[ridx], yMax) ? 1 : 0))
102104

103-
return {
104-
animate: true,
105-
rows: filteredRows,
106-
counts: filteredRows.map(countOf),
107-
scale: filteredRows.length === rows.length ? 'linear' : 'log'
105+
const filteredRowsPriorToSorting = rows.filter((row, ridx) => (!Histogram.isTiny(counts[ridx], yMax) ? 1 : 0))
106+
107+
if (!useFancyColoring) {
108+
const filteredRows = filteredRowsPriorToSorting
109+
return {
110+
animate: true,
111+
rows: filteredRows,
112+
counts: filteredRows.map(countOf),
113+
scale: filteredRows.length === rows.length ? 'linear' : 'log',
114+
colors: undefined
115+
}
116+
} else {
117+
// assign colors to the rows, and then sort the rows to group
118+
// nearby colors(in the color space) so that they are also close
119+
// geographically
120+
const sortedByCount = filteredRowsPriorToSorting.slice().sort((a, b) => countOf(b) - countOf(a))
121+
const colors = stringColoring(sortedByCount.map(_ => _.rowKey || _.name))
122+
123+
const filteredRowsForSorting = sortedByCount
124+
.map((row, idx) => ({ row, color: colors[idx] }))
125+
.sort((a, b) => b.color.primary - a.color.primary || b.color.secondary - a.color.secondary)
126+
127+
const filteredRows = filteredRowsForSorting.map(_ => _.row)
128+
129+
return {
130+
animate: true,
131+
rows: filteredRows,
132+
counts: filteredRows.map(countOf),
133+
scale: filteredRows.length === rows.length ? 'linear' : 'log',
134+
colors: filteredRowsForSorting.map(_ => _.color.color)
135+
}
108136
}
109137
}
110138

@@ -140,15 +168,15 @@ export default class Histogram extends React.PureComponent<Props, State> {
140168
<Chart
141169
animate={this.state.animate && { onLoad: { duration: 0 }, duration: 200 }}
142170
domainPadding={10}
143-
height={this.state.rows.length * this.barHeight * 1.375}
171+
height={this.state.rows.length * this.barHeight * (1 + this.barSpacing)}
144172
horizontal={this.horizontal}
145173
padding={{
146174
left: this.leftPad(),
147175
right: this.rightPad(),
148176
top: 0,
149177
bottom: 0
150178
}}
151-
containerComponent={<ChartVoronoiContainer labels={_ => `${_.datum.x}: ${_.datum.y}`} constrainToVisibleArea />}
179+
containerComponent={<ChartVoronoiContainer constrainToVisibleArea />}
152180
>
153181
{this.axis()}
154182
{this.bars()}
@@ -181,7 +209,13 @@ export default class Histogram extends React.PureComponent<Props, State> {
181209
barWidth={this.barHeight}
182210
scale={{ y: this.state.scale }}
183211
style={{
184-
data: { fill: 'var(--color-base05)', stroke: 'var(--color-base04)', strokeWidth: 0.5 },
212+
data: {
213+
fill: !this.state.colors
214+
? 'var(--color-base05)'
215+
: ({ index }) => this.state.colors[index] || 'var(--color-base05)',
216+
stroke: !this.state.colors ? 'var(--color-base04)' : undefined,
217+
strokeWidth: 0.5
218+
},
185219
labels: { fontFamily: 'var(--font-sans-serif)', fontSize: this.barLabelFontSize }
186220
}}
187221
data={this.state.rows.map((row, ridx) => ({

plugins/plugin-client-common/src/controller/grammy.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import PromisePool from '@supercharge/promise-pool'
1818

19-
import { Arguments, i18n, Row, Registrar, Table, UsageModel, encodeComponent } from '@kui-shell/core'
19+
import { Arguments, ParsedOptions, i18n, Row, Registrar, Table, UsageModel, encodeComponent } from '@kui-shell/core'
2020
const strings = i18n('plugin-client-common')
2121

2222
/**
@@ -26,10 +26,15 @@ const usage: UsageModel = {
2626
command: 'grammy',
2727
strict: 'grammy',
2828
example: 'grammy filepath',
29-
docs: 'Grammy'
29+
docs: 'Grammy',
30+
optional: [{ name: '--color', alias: '-c', boolean: true }]
3031
}
3132

32-
async function doHistogram(args: Arguments): Promise<Table> {
33+
interface Options extends ParsedOptions {
34+
color: boolean
35+
}
36+
37+
async function doHistogram(args: Arguments<Options>): Promise<Table> {
3338
const { REPL, argvNoOptions } = args
3439
const filepath = argvNoOptions[1]
3540

@@ -109,7 +114,8 @@ async function doHistogram(args: Arguments): Promise<Table> {
109114
body,
110115
header,
111116
title: strings('Histogram'),
112-
defaultPresentation: 'histogram'
117+
defaultPresentation: 'histogram',
118+
colorBy: args.parsedOptions.color ? 'default' : undefined
113119
}
114120
} else {
115121
throw new Error('grammy: file not provided')
@@ -121,5 +127,5 @@ async function doHistogram(args: Arguments): Promise<Table> {
121127
*
122128
*/
123129
export default async (commandTree: Registrar) => {
124-
commandTree.listen('/grammy', doHistogram, { usage })
130+
commandTree.listen('/grammy', doHistogram, { usage, flags: { boolean: ['color', 'c'] } })
125131
}

0 commit comments

Comments
 (0)