Skip to content

Commit 26c886c

Browse files
Merge pull request #621 from rokotyan/feature/axis-min-max-grid-lines
Axis: Show grid when `minMaxTicksOnly` is `true`
2 parents 04a6c23 + 6b0eaf5 commit 26c886c

File tree

6 files changed

+143
-22
lines changed

6 files changed

+143
-22
lines changed

packages/angular/src/components/axis/axis.component.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ export class VisAxisComponent<Datum> implements AxisConfigInterface<Datum>, Afte
8989
/** Distance between the axis and the label in pixels. Default: `8` */
9090
@Input() labelMargin?: number
9191

92+
/** Label text fit mode: `FitMode.Wrap` or `FitMode.Trim`. Default: `FitMode.Wrap`. */
93+
@Input() labelTextFitMode?: FitMode | string
94+
95+
/** Label text trim mode: `TrimMode.Start`, `TrimMode.Middle` or `TrimMode.End`. Default: `TrimMode.Middle` */
96+
@Input() labelTextTrimType?: TrimMode | string
97+
9298
/** Font color of the axis label as CSS string. Default: `null` */
9399
@Input() labelColor?: string | null
94100

@@ -104,6 +110,9 @@ export class VisAxisComponent<Datum> implements AxisConfigInterface<Datum>, Afte
104110
/** Draw only the min and max axis ticks. Default: `false` */
105111
@Input() minMaxTicksOnly?: boolean
106112

113+
/** Show grid lines for the min and max axis ticks. Default: `false` */
114+
@Input() minMaxTicksOnlyShowGridLines?: boolean
115+
107116
/** Draw only the min and max axis ticks, when the chart
108117
* width is less than the specified value.
109118
* Default: `250` */
@@ -174,8 +183,8 @@ export class VisAxisComponent<Datum> implements AxisConfigInterface<Datum>, Afte
174183
}
175184

176185
private getConfig (): AxisConfigInterface<Datum> {
177-
const { duration, events, attributes, position, type, fullSize, label, labelFontSize, labelMargin, labelColor, gridLine, tickLine, domainLine, minMaxTicksOnly, minMaxTicksOnlyWhenWidthIsLess, tickFormat, tickValues, numTicks, tickTextFitMode, tickTextWidth, tickTextSeparator, tickTextForceWordBreak, tickTextTrimType, tickTextFontSize, tickTextAlign, tickTextColor, tickTextAngle, tickTextHideOverlapping, tickPadding } = this
178-
const config = { duration, events, attributes, position, type, fullSize, label, labelFontSize, labelMargin, labelColor, gridLine, tickLine, domainLine, minMaxTicksOnly, minMaxTicksOnlyWhenWidthIsLess, tickFormat, tickValues, numTicks, tickTextFitMode, tickTextWidth, tickTextSeparator, tickTextForceWordBreak, tickTextTrimType, tickTextFontSize, tickTextAlign, tickTextColor, tickTextAngle, tickTextHideOverlapping, tickPadding }
186+
const { duration, events, attributes, position, type, fullSize, label, labelFontSize, labelMargin, labelTextFitMode, labelTextTrimType, labelColor, gridLine, tickLine, domainLine, minMaxTicksOnly, minMaxTicksOnlyShowGridLines, minMaxTicksOnlyWhenWidthIsLess, tickFormat, tickValues, numTicks, tickTextFitMode, tickTextWidth, tickTextSeparator, tickTextForceWordBreak, tickTextTrimType, tickTextFontSize, tickTextAlign, tickTextColor, tickTextAngle, tickTextHideOverlapping, tickPadding } = this
187+
const config = { duration, events, attributes, position, type, fullSize, label, labelFontSize, labelMargin, labelTextFitMode, labelTextTrimType, labelColor, gridLine, tickLine, domainLine, minMaxTicksOnly, minMaxTicksOnlyShowGridLines, minMaxTicksOnlyWhenWidthIsLess, tickFormat, tickValues, numTicks, tickTextFitMode, tickTextWidth, tickTextSeparator, tickTextForceWordBreak, tickTextTrimType, tickTextFontSize, tickTextAlign, tickTextColor, tickTextAngle, tickTextHideOverlapping, tickPadding }
179188
const keys = Object.keys(config) as (keyof AxisConfigInterface<Datum>)[]
180189
keys.forEach(key => { if (config[key] === undefined) delete config[key] })
181190

packages/angular/src/html-components/bullet-legend/bullet-legend.component.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export class VisBulletLegendComponent implements BulletLegendConfigInterface, Af
4949
/** Bullet shape size, mapped to the width and height CSS properties. Default: `null` */
5050
@Input() bulletSize?: string | null
5151

52+
/** Spacing between multiple bullet symbols in pixels. Default: `4` */
53+
@Input() bulletSpacing?: number
54+
5255
/** Bullet shape enum value or accessor function. Default: `d => d.shape ?? BulletShape.Circle */
5356
@Input() bulletShape?: GenericAccessor<BulletShape, BulletLegendItemInterface>
5457

@@ -67,8 +70,8 @@ export class VisBulletLegendComponent implements BulletLegendConfigInterface, Af
6770
}
6871

6972
private getConfig (): BulletLegendConfigInterface {
70-
const { items, labelClassName, onLegendItemClick, labelFontSize, labelMaxWidth, bulletSize, bulletShape, orientation } = this
71-
const config = { items, labelClassName, onLegendItemClick, labelFontSize, labelMaxWidth, bulletSize, bulletShape, orientation }
73+
const { items, labelClassName, onLegendItemClick, labelFontSize, labelMaxWidth, bulletSize, bulletSpacing, bulletShape, orientation } = this
74+
const config = { items, labelClassName, onLegendItemClick, labelFontSize, labelMaxWidth, bulletSize, bulletSpacing, bulletShape, orientation }
7275
const keys = Object.keys(config) as (keyof BulletLegendConfigInterface)[]
7376
keys.forEach(key => { if (config[key] === undefined) delete config[key] })
7477

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React, { useCallback, useMemo } from 'react'
2+
import { VisXYContainer, VisAxis, VisLine } from '@unovis/react'
3+
import { XYDataRecord, generateXYDataRecords } from '@src/utils/data'
4+
import { ExampleViewerDurationProps } from '@src/components/ExampleViewer/index'
5+
6+
export const title = 'Axis Min-Max Ticks Only'
7+
export const subTitle = 'Show Grid Lines'
8+
9+
export const component = (props: ExampleViewerDurationProps): React.ReactNode => {
10+
const accessors = useMemo(() => [
11+
(d: XYDataRecord) => d.y,
12+
(d: XYDataRecord) => d.y1,
13+
(d: XYDataRecord) => d.y2,
14+
], [])
15+
16+
const data = useMemo(() => generateXYDataRecords(15), [])
17+
18+
const tickFormatter = useCallback((tick: number | Date) => `${(+tick).toFixed(1)}`, [])
19+
return (
20+
<>
21+
<code>Default: No Grid Lines</code>
22+
<VisXYContainer<XYDataRecord> data={data}>
23+
<VisLine x={d => d.x} y={accessors} duration={props.duration}/>
24+
<VisAxis type='x' tickFormat={tickFormatter} duration={props.duration} minMaxTicksOnly={true} />
25+
<VisAxis type='y' tickFormat={tickFormatter} duration={props.duration} minMaxTicksOnly={true} />
26+
</VisXYContainer>
27+
28+
<code><b>minMaxTicksOnlyShowGridLines: true</b></code>
29+
<VisXYContainer<XYDataRecord> data={data}>
30+
<VisLine x={d => d.x} y={accessors} duration={props.duration}/>
31+
<VisAxis type='x' tickFormat={tickFormatter} duration={props.duration} minMaxTicksOnly={true} minMaxTicksOnlyShowGridLines={true}/>
32+
<VisAxis type='y' tickFormat={tickFormatter} duration={props.duration} minMaxTicksOnly={true} minMaxTicksOnlyShowGridLines={true}/>
33+
</VisXYContainer>
34+
35+
<code><b>minMaxTicksOnlyShowGridLines: true</b><br />
36+
Custom tickValues for xAxis: [0, 2, 4, 6]. <br />
37+
No grid line at 10.2, because it's too close to the previous grid line.
38+
</code>
39+
<VisXYContainer<XYDataRecord> data={data} yDomain={[0, 10.2]}>
40+
<VisLine x={d => d.x} y={accessors} duration={props.duration}/>
41+
<VisAxis
42+
type='x'
43+
tickFormat={tickFormatter}
44+
duration={props.duration}
45+
minMaxTicksOnly={true}
46+
minMaxTicksOnlyShowGridLines={true}
47+
tickValues={[0, 2, 4, 6]}
48+
/>
49+
<VisAxis type='y' tickFormat={tickFormatter} duration={props.duration} minMaxTicksOnly={true} minMaxTicksOnlyShowGridLines={true}/>
50+
</VisXYContainer>
51+
</>
52+
)
53+
}

packages/ts/src/components/axis/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export interface AxisConfigInterface<Datum> extends Partial<XYComponentConfigInt
3333
domainLine?: boolean;
3434
/** Draw only the min and max axis ticks. Default: `false` */
3535
minMaxTicksOnly?: boolean;
36+
/** Show grid lines for the min and max axis ticks. Default: `false` */
37+
minMaxTicksOnlyShowGridLines?: boolean;
3638
/** Draw only the min and max axis ticks, when the chart
3739
* width is less than the specified value.
3840
* Default: `250` */
@@ -84,6 +86,7 @@ export const AxisDefaultConfig: AxisConfigInterface<unknown> = {
8486
numTicks: undefined,
8587
minMaxTicksOnly: false,
8688
minMaxTicksOnlyWhenWidthIsLess: 250,
89+
minMaxTicksOnlyShowGridLines: false,
8790
tickTextWidth: undefined,
8891
tickTextSeparator: undefined,
8992
tickTextForceWordBreak: false,

packages/ts/src/components/axis/index.ts

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { select, Selection } from 'd3-selection'
22
import { interrupt } from 'd3-transition'
33
import { Axis as D3Axis, axisBottom, axisLeft, axisRight, axisTop } from 'd3-axis'
4+
import { NumberValue } from 'd3-scale'
45

56
// Core
67
import { XYComponentCore } from 'core/xy-component'
@@ -137,8 +138,7 @@ export class Axis<Datum> extends XYComponentCore<Datum, AxisConfigInterface<Datu
137138
this._renderAxisLabel(selection)
138139

139140
if (config.gridLine) {
140-
const gridGen = this._buildGrid().tickFormat(() => '')
141-
gridGen.tickValues(this._getConfiguredTickValues())
141+
const gridGen = this._buildGrid()
142142
// Interrupting all active transitions first to prevent them from being stuck.
143143
// Somehow we see it happening in Angular apps.
144144
this.gridGroup.selectAll('*').interrupt()
@@ -169,29 +169,70 @@ export class Axis<Datum> extends XYComponentCore<Datum, AxisConfigInterface<Datu
169169
}
170170
}
171171

172-
private _buildGrid (): D3Axis<any> {
173-
const { config: { type, position } } = this
172+
private _buildGrid (): D3Axis<NumberValue | Date> {
173+
const { config } = this
174174

175-
const ticks = this._getNumTicks()
176-
switch (type) {
175+
let gridGen: D3Axis<NumberValue | Date>
176+
switch (config.type) {
177177
case AxisType.X:
178-
switch (position) {
179-
case Position.Top: return axisTop(this.xScale).ticks(ticks * 2).tickSize(-this._height).tickSizeOuter(0)
180-
case Position.Bottom: default: return axisBottom(this.xScale).ticks(ticks * 2).tickSize(-this._height).tickSizeOuter(0)
178+
switch (config.position) {
179+
case Position.Top: { gridGen = axisTop(this.xScale); break }
180+
case Position.Bottom: default: { gridGen = axisBottom(this.xScale); break }
181181
}
182+
gridGen.tickSize(-this._height)
183+
break
182184
case AxisType.Y:
183-
switch (position) {
184-
case Position.Right: return axisRight(this.yScale).ticks(ticks * 2).tickSize(-this._width).tickSizeOuter(0)
185-
case Position.Left: default: return axisLeft(this.yScale).ticks(ticks * 2).tickSize(-this._width).tickSizeOuter(0)
185+
switch (config.position) {
186+
case Position.Right: { gridGen = axisRight(this.yScale); break }
187+
case Position.Left: default: { gridGen = axisLeft(this.yScale); break }
186188
}
189+
gridGen.tickSize(-this._width)
187190
}
191+
gridGen
192+
.tickSizeOuter(0)
193+
.tickFormat(() => '')
194+
195+
const numTicks = this._getNumTicks() * 2
196+
const gridScale = gridGen.scale<ContinuousScale>()
197+
const scaleDomain = gridScale.domain()
198+
199+
const getGridMinMaxTicksOnlyValues = (): number[] | Date[] => {
200+
if (!config.minMaxTicksOnlyShowGridLines) return scaleDomain
201+
202+
const tickValues = gridScale.ticks(numTicks)
203+
if (tickValues.length < 2) return scaleDomain
204+
205+
// If the last tick is far enough from the domain max value, we add it to the tick values to draw the grid line
206+
const tickValuesStep = +tickValues[1] - +tickValues[0]
207+
const domainMaxValue = scaleDomain[1]
208+
const diff = +domainMaxValue - +tickValues[tickValues.length - 1]
209+
210+
return diff > tickValuesStep / 2 ? [...tickValues, domainMaxValue] as (number[] | Date[]) : tickValues
211+
}
212+
213+
const tickValues = config.tickValues
214+
? this._getConfiguredTickValues()
215+
: this._shouldRenderMinMaxTicksOnly()
216+
? getGridMinMaxTicksOnlyValues()
217+
: gridScale.ticks(numTicks)
218+
219+
gridGen.tickValues(tickValues)
220+
221+
return gridGen
188222
}
189223

190224
private _renderAxis (selection = this.axisGroup, duration = this.config.duration): void {
191225
const { config } = this
192226

193227
const axisGen = this._buildAxis()
194-
const tickValues: (number[] | Date[]) = this._getConfiguredTickValues() || axisGen.scale<ContinuousScale>().ticks(this._getNumTicks())
228+
const axisScale = axisGen.scale<ContinuousScale>()
229+
const tickValues: (number[] | Date[]) =
230+
config.tickValues
231+
? this._getConfiguredTickValues()
232+
: this._shouldRenderMinMaxTicksOnly()
233+
? axisScale.domain()
234+
: axisScale.ticks(this._getNumTicks())
235+
195236
axisGen.tickValues(tickValues)
196237

197238
// Interrupting all active transitions first to prevent them from being stuck.
@@ -344,13 +385,14 @@ export class Axis<Datum> extends XYComponentCore<Datum, AxisConfigInterface<Datu
344385
return config.tickValues.filter(v => (v >= scaleDomain[0]) && (v <= scaleDomain[1]))
345386
}
346387

347-
if (config.minMaxTicksOnly || (config.type === AxisType.X && this._width < config.minMaxTicksOnlyWhenWidthIsLess)) {
348-
return scaleDomain as number[]
349-
}
350-
351388
return null
352389
}
353390

391+
private _shouldRenderMinMaxTicksOnly (): boolean {
392+
const { config } = this
393+
return config.minMaxTicksOnly || (config.type === AxisType.X && this._width < config.minMaxTicksOnlyWhenWidthIsLess)
394+
}
395+
354396
private _getFullDomainPath (tickSize = 0): string {
355397
const { config: { type } } = this
356398
switch (type) {

packages/website/docs/auxiliary/Axis.mdx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ The specified count is only a hint, the axis can have more or fewer ticks depend
214214
Set the `minMaxTicksOnly` property to `true` if you only want to see the two end ticks on the axis.
215215

216216
:::note
217-
To display the minimum and maximum ticks only when the chart width is limited (this behavior is enabed my default),
217+
To display the minimum and maximum ticks only when the chart width is limited (this behavior is enabled my default),
218218
you can use the `minMaxTicksOnlyWhenWidthIsLess` property (defaults to 250px). This helps avoid clutter in smaller visualizations while still
219219
providing essential information.
220220
:::
@@ -226,6 +226,17 @@ providing essential information.
226226
property="minMaxTicksOnly"/>
227227

228228

229+
When using `minMaxTicksOnly`, you can still show grid lines by setting `minMaxTicksOnlyShowGridLines` to `true`.
230+
231+
<XYWrapperWithInput
232+
minMaxTicksOnly={true}
233+
{...axisProps()}
234+
inputType="checkbox"
235+
defaultValue={true}
236+
property="minMaxTicksOnlyShowGridLines"
237+
hiddenProps={{}}
238+
/>
239+
229240
### Set Ticks Explicitly
230241
You can customize the ticks displayed by providing the _Axis_ component with a number array.
231242
The following example only shows even values for x after getting the `tickValue` array from a filter function.

0 commit comments

Comments
 (0)