-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* [#181971361] Feature: Date attributes can be plotted on a date-time axis This is a "first pass" implementation of a date-time axis. Missing features and bugs should be logged in separate PT stories unless they break non-date-time axes or otherwise disrupt normal V3 behavior. * Detect "date" as a valid attribute type when setting primary type in setPrimaryRoleAndPlotType * Define `AbstractNumericAxisModel` with descendant `NumericAxisModel` * Add DateAxisModel as a descendant of `AbstractNumericAxisModel` * Allow for creation of DatAxisModel in `setupAxes` * Defined AxisHelper and subclasses for empty, numeric and categorical. Create these in useSubAxis. * DateAxisHelper is responsible for rendering the data axis scale * Both NumericAxisModel and DateAxisModel return true for the new test `isAbstractNumeric` * Added data-display-value-utils.ts with dataDisplayGetNumericValue as its only member (so far) to lessen likelihood of Lakosian violations * To get drag rescale working, define `isAbstractNumericAxisModel` and use it, where appropriate, in place of `isNumericAxisModel` * * Code review changes * * Code review changes * Added axis helper jest tests
- Loading branch information
Showing
40 changed files
with
1,330 additions
and
361 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
// Tests for the AxisHelper class | ||
import { select } from "d3" | ||
import { AxisHelper, IAxisHelperArgs } from "./axis-helper" | ||
import { IAxisModel } from "../models/axis-model" | ||
import { IDataDisplayContentModel } from "../../data-display/models/data-display-content-model" | ||
import { IAxisLayout } from "../models/axis-layout-context" | ||
|
||
jest.mock("d3", () => ({ | ||
select: jest.fn().mockReturnValue({ | ||
selectAll: jest.fn().mockReturnValue({ | ||
remove: jest.fn() | ||
}), | ||
attr: jest.fn().mockReturnThis(), | ||
append: jest.fn().mockReturnThis(), | ||
style: jest.fn().mockReturnThis() | ||
}) | ||
})) | ||
|
||
describe("AxisHelper", () => { | ||
let props: IAxisHelperArgs | ||
let axisHelper: AxisHelper | ||
|
||
beforeEach(() => { | ||
props = { | ||
displayModel: {} as IDataDisplayContentModel, | ||
subAxisIndex: 0, | ||
subAxisElt: document.createElementNS("http://www.w3.org/2000/svg", "g"), | ||
axisModel: { place: "left" } as IAxisModel, | ||
layout: { | ||
getAxisMultiScale: jest.fn().mockReturnValue({ cellLength: 100 }), | ||
getComputedBounds: jest.fn().mockReturnValue({ left: 0, top: 0, width: 100, height: 100 }) | ||
} as unknown as IAxisLayout, | ||
isAnimating: jest.fn().mockReturnValue(false) | ||
} | ||
axisHelper = new AxisHelper(props) | ||
}) | ||
|
||
it("should initialize correctly", () => { | ||
expect(axisHelper.displayModel).toBe(props.displayModel) | ||
expect(axisHelper.subAxisIndex).toBe(props.subAxisIndex) | ||
expect(axisHelper.subAxisElt).toBe(props.subAxisElt) | ||
expect(axisHelper.axisModel).toBe(props.axisModel) | ||
expect(axisHelper.layout).toBe(props.layout) | ||
expect(axisHelper.isAnimating).toBe(props.isAnimating) | ||
expect(axisHelper.multiScale).toEqual({ cellLength: 100 }) | ||
}) | ||
|
||
it("should return correct axisPlace", () => { | ||
expect(axisHelper.axisPlace).toBe("left") | ||
}) | ||
|
||
it("should return correct dataConfig", () => { | ||
expect(axisHelper.dataConfig).toBe(props.displayModel.dataConfiguration) | ||
}) | ||
|
||
it("should return correct initialTransform", () => { | ||
expect(axisHelper.initialTransform).toBe("translate(100, 0)") | ||
}) | ||
|
||
it("should return correct isVertical", () => { | ||
expect(axisHelper.isVertical).toBe(true) | ||
}) | ||
|
||
it("should return correct subAxisLength", () => { | ||
expect(axisHelper.subAxisLength).toBe(100) | ||
}) | ||
|
||
it("should return correct axis", () => { | ||
expect(axisHelper.axisModel).toBeDefined() | ||
}) | ||
|
||
it("should return correct rangeMin", () => { | ||
expect(axisHelper.rangeMin).toBe(0) | ||
}) | ||
|
||
it("should return correct rangeMax", () => { | ||
expect(axisHelper.rangeMax).toBe(100) | ||
}) | ||
|
||
it("should render axis line correctly", () => { | ||
axisHelper.renderAxisLine() | ||
expect(select).toHaveBeenCalledWith(props.subAxisElt) | ||
expect(select(props.subAxisElt).selectAll).toHaveBeenCalledWith('*') | ||
expect(select(props.subAxisElt).attr).toHaveBeenCalledWith("transform", "translate(100, 0)") | ||
expect(select(props.subAxisElt).append).toHaveBeenCalledWith('line') | ||
expect(select(props.subAxisElt).style).toHaveBeenCalledWith("stroke", "darkgray") | ||
expect(select(props.subAxisElt).style).toHaveBeenCalledWith("stroke-opacity", "0.7") | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { select } from "d3" | ||
import { isVertical } from "../../axis-graph-shared" | ||
import { AxisBounds, axisPlaceToAxisFn } from "../axis-types" | ||
import { IAxisModel } from "../models/axis-model" | ||
import { IDataDisplayContentModel } from "../../data-display/models/data-display-content-model" | ||
import { IAxisLayout } from "../models/axis-layout-context" | ||
import { MultiScale } from "../models/multi-scale" | ||
|
||
export interface IAxisHelperArgs { | ||
displayModel: IDataDisplayContentModel | ||
subAxisIndex: number | ||
subAxisElt: SVGGElement | null | ||
axisModel: IAxisModel | ||
layout: IAxisLayout | ||
isAnimating: () => boolean | ||
} | ||
|
||
export class AxisHelper { | ||
displayModel: IDataDisplayContentModel | ||
subAxisIndex: number | ||
subAxisElt: SVGGElement | null | ||
axisModel: IAxisModel | ||
layout: IAxisLayout | ||
isAnimating: () => boolean | ||
multiScale: MultiScale | undefined | ||
|
||
constructor(props: IAxisHelperArgs) { | ||
this.displayModel = props.displayModel | ||
this.subAxisIndex = props.subAxisIndex | ||
this.subAxisElt = props.subAxisElt | ||
this.axisModel = props.axisModel | ||
this.layout = props.layout | ||
this.isAnimating = props.isAnimating | ||
this.multiScale = this.layout.getAxisMultiScale(this.axisPlace) | ||
} | ||
|
||
get axisPlace() { | ||
return this.axisModel.place | ||
} | ||
|
||
get dataConfig() { | ||
return this.displayModel.dataConfiguration | ||
} | ||
|
||
get initialTransform() { | ||
const axisBounds = this.layout.getComputedBounds(this.axisPlace) as AxisBounds | ||
return (this.axisPlace === 'left') | ||
? `translate(${axisBounds.left + axisBounds.width}, ${axisBounds.top})` | ||
: (this.axisPlace === 'top') | ||
? `translate(${axisBounds.left}, ${axisBounds.top + axisBounds.height})` | ||
: `translate(${axisBounds.left}, ${axisBounds.top})` | ||
} | ||
|
||
get isVertical() { | ||
return isVertical(this.axisPlace) | ||
} | ||
|
||
get subAxisLength() { | ||
return this.multiScale?.cellLength ?? 0 | ||
} | ||
|
||
get axis() { | ||
return axisPlaceToAxisFn(this.axisPlace) | ||
} | ||
|
||
get rangeMin() { | ||
return this.subAxisIndex * this.subAxisLength | ||
} | ||
|
||
get rangeMax() { | ||
return this.rangeMin + this.subAxisLength | ||
} | ||
|
||
renderAxisLine() { | ||
select(this.subAxisElt).selectAll('*').remove() | ||
select(this.subAxisElt) | ||
.attr("transform", this.initialTransform) | ||
.append('line') | ||
.attr('x1', 0) | ||
.attr('x2', this.isVertical ? 0 : this.subAxisLength) | ||
.attr('y1', 0) | ||
.attr('y2', this.isVertical ? this.subAxisLength : 0) | ||
.style("stroke", "darkgray") | ||
.style("stroke-opacity", "0.7") | ||
} | ||
|
||
} | ||
|
||
export class EmptyAxisHelper extends AxisHelper { | ||
|
||
render() { | ||
this.renderAxisLine() | ||
} | ||
} |
122 changes: 122 additions & 0 deletions
122
v3/src/components/axis/helper-models/categorical-axis-helper.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { BaseType, Selection } from "d3" | ||
import { AxisHelper, IAxisHelperArgs } from "./axis-helper" | ||
import { MutableRefObject } from "react" | ||
import { kAxisTickLength } from "../../graph/graphing-types" | ||
import { otherPlace } from "../axis-types" | ||
import { axisPlaceToAttrRole, transitionDuration } from "../../data-display/data-display-types" | ||
import { | ||
collisionExists, DragInfo, | ||
getCategoricalLabelPlacement, | ||
getCoordFunctions, | ||
IGetCoordFunctionsProps | ||
} from "../axis-utils" | ||
|
||
export interface CatObject { | ||
cat: string | ||
index: number | ||
} | ||
|
||
export interface ICategoricalAxisHelperArgs extends IAxisHelperArgs { | ||
subAxisSelectionRef: MutableRefObject<Selection<SVGGElement, any, any, any> | undefined> | ||
categoriesSelectionRef: MutableRefObject<Selection<SVGGElement | BaseType, CatObject, SVGGElement, any> | undefined> | ||
swapInProgress: MutableRefObject<boolean> | ||
centerCategoryLabels: boolean | ||
dragInfo: MutableRefObject<DragInfo> | ||
} | ||
|
||
export class CategoricalAxisHelper extends AxisHelper { | ||
subAxisSelectionRef: MutableRefObject<Selection<SVGGElement, any, any, any> | undefined> | ||
categoriesSelectionRef: MutableRefObject<Selection<SVGGElement | BaseType, CatObject, SVGGElement, any> | undefined> | ||
swapInProgress: MutableRefObject<boolean> | ||
centerCategoryLabels: boolean | ||
dragInfo: MutableRefObject<DragInfo> | ||
|
||
constructor(props: ICategoricalAxisHelperArgs) { | ||
super(props) | ||
this.subAxisSelectionRef = props.subAxisSelectionRef | ||
this.categoriesSelectionRef = props.categoriesSelectionRef | ||
this.swapInProgress = props.swapInProgress | ||
this.centerCategoryLabels = props.centerCategoryLabels | ||
this.dragInfo = props.dragInfo | ||
} | ||
|
||
render() { | ||
if (!(this.subAxisSelectionRef.current && this.categoriesSelectionRef.current)) return | ||
|
||
const {isVertical, centerCategoryLabels, dragInfo} = this, | ||
categorySet = this.multiScale?.categorySet, | ||
dividerLength = this.layout.getAxisLength(otherPlace(this.axisPlace)) ?? 0, | ||
isRightCat = this.axisPlace === 'rightCat', | ||
isTop = this.axisPlace === 'top', | ||
role = axisPlaceToAttrRole[this.axisPlace], | ||
categories: string[] = this.dataConfig?.categoryArrayForAttrRole(role) ?? [], | ||
numCategories = categories.length, | ||
hasCategories = !(categories.length === 1 && categories[0] === "__main__"), | ||
bandWidth = this.subAxisLength / numCategories, | ||
collision = collisionExists({bandWidth, categories, centerCategoryLabels}), | ||
{rotation, textAnchor} = getCategoricalLabelPlacement(this.axisPlace, this.centerCategoryLabels, | ||
collision), | ||
duration = (this.isAnimating() && !this.swapInProgress.current && | ||
dragInfo.current.indexOfCategory === -1) ? transitionDuration : 0 | ||
|
||
// Fill out dragInfo for use in drag callbacks | ||
const dI = dragInfo.current | ||
dI.categorySet = categorySet | ||
dI.categories = categories | ||
dI.bandwidth = bandWidth | ||
dI.axisOrientation = isVertical ? 'vertical' : 'horizontal' | ||
dI.labelOrientation = isVertical ? (collision ? 'horizontal' : 'vertical') | ||
: (collision ? 'vertical' : 'horizontal') | ||
|
||
const sAS = this.subAxisSelectionRef.current, | ||
{ rangeMin, rangeMax, subAxisLength } = this | ||
|
||
sAS.attr("transform", this.initialTransform) | ||
.select('line') | ||
.attr('x1', isVertical ? 0 : rangeMin) | ||
.attr('x2', isVertical ? 0 : rangeMax) | ||
.attr('y1', isVertical ? rangeMin : 0) | ||
.attr('y2', isVertical ? rangeMax : 0) | ||
|
||
const props: IGetCoordFunctionsProps = { | ||
numCategories, centerCategoryLabels, collision, axisIsVertical: isVertical, rangeMin, rangeMax, | ||
subAxisLength, isRightCat, isTop, dragInfo | ||
}, | ||
fns = getCoordFunctions(props) | ||
|
||
hasCategories && this.categoriesSelectionRef.current | ||
.join( | ||
enter => enter, | ||
update => { | ||
update.select('.tick') | ||
.attr('x1', (d, i) => fns.getTickX(i)) | ||
.attr('x2', (d, i) => isVertical | ||
? (isRightCat ? 1 : -1) * kAxisTickLength : fns.getTickX(i)) | ||
.attr('y1', (d, i) => fns.getTickY(i)) | ||
.attr('y2', (d, i) => isVertical | ||
? fns.getTickY(i) : (isTop ? -1 : 1) * kAxisTickLength) | ||
// divider between groups | ||
update.select('.divider') | ||
.attr('x1', (d, i) => fns.getDividerX(i)) | ||
.attr('x2', (d, i) => isVertical | ||
? (isRightCat ? -1 : 1) * dividerLength : fns.getDividerX(i)) | ||
.attr('y1', (d, i) => fns.getDividerY(i)) | ||
.attr('y2', (d, i) => isVertical | ||
? fns.getDividerY(i) : (isTop ? 1 : -1) * dividerLength) | ||
// labels | ||
update.select('.category-label') | ||
.attr('transform', `${rotation}`) | ||
.attr('text-anchor', textAnchor) | ||
.attr('transform-origin', (d, i) => { | ||
return `${fns.getLabelX(i)} ${fns.getLabelY(i)}` | ||
}) | ||
.transition().duration(duration) | ||
.attr('class', 'category-label') | ||
.attr('x', (d, i) => fns.getLabelX(i)) | ||
.attr('y', (d, i) => fns.getLabelY(i)) | ||
.text((catObject: CatObject) => String(catObject.cat)) | ||
return update | ||
} | ||
) | ||
} | ||
} |
Oops, something went wrong.