Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ability to lock axes #185

Merged
merged 11 commits into from
Sep 11, 2023
39 changes: 32 additions & 7 deletions app/static/src/app/components/WodinPlot.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="wodin-plot-container" :style="plotStyle">
<div class="plot" ref="plot">
<div class="plot" ref="plot" id="plot">
</div>
<div v-if="!hasPlotData" class="plot-placeholder">
{{ placeholderMessage }}
Expand All @@ -17,12 +17,14 @@ import {
import { useStore } from "vuex";
import { EventEmitter } from "events";
import {
newPlot, react, PlotRelayoutEvent, Plots, AxisType
newPlot, react, PlotRelayoutEvent, Plots, AxisType, Layout, Config
} from "plotly.js-basic-dist-min";
import {
WodinPlotData, fadePlotStyle, margin, config
} from "../plot";
import WodinPlotDataSummary from "./WodinPlotDataSummary.vue";
import { GraphSettingsMutation } from "../store/graphSettings/mutations";
import { YAxisRange } from "../store/graphSettings/state";

export default defineComponent({
name: "WodinPlot",
Expand Down Expand Up @@ -64,6 +66,16 @@ export default defineComponent({
const hasPlotData = computed(() => !!(baseData.value?.length));

const yAxisType = computed(() => (store.state.graphSettings.logScaleYAxis ? "log" : "linear" as AxisType));
const lockYAxis = computed(() => store.state.graphSettings.lockYAxis);
const yAxisRange = computed(() => store.state.graphSettings.yAxisRange as YAxisRange);

const updateAxesRange = () => {
const plotLayout = (plot.value as any).layout;
const yRange = plotLayout.yaxis?.range;
if (plotLayout) {
store.commit(`graphSettings/${GraphSettingsMutation.SetYAxisRange}`, yRange);
}
};

const relayout = async (event: PlotRelayoutEvent) => {
let data;
Expand All @@ -78,7 +90,7 @@ export default defineComponent({
data = props.plotData(t0, t1, nPoints);
}

const layout = {
const layout: Partial<Layout> = {
margin,
uirevision: "true",
xaxis: { title: "Time", autorange: true },
Expand All @@ -97,21 +109,33 @@ export default defineComponent({

let resizeObserver: null | ResizeObserver = null;

const drawPlot = () => {
const drawPlot = (toggleLogScale = false) => {
if (props.redrawWatches.length) {
baseData.value = props.plotData(startTime, props.endTime, nPoints);

if (hasPlotData.value) {
const el = plot.value as unknown;
const layout = {
const layout: Partial<Layout> = {
margin,
yaxis: {
type: yAxisType.value
},
xaxis: { title: "Time" }
};

newPlot(el as HTMLElement, baseData.value, layout, config);
const configCopy = { ...config } as Partial<Config>;

if (lockYAxis.value && !toggleLogScale) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this means it always does autorange if we're updating because of logScale value change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes so the log scale completely changes the axis, if you lock the axis as before there are very weird things y axis values, so instead we don't lock y axis when they toggle between the log and normal scale

layout.yaxis!.range = [...yAxisRange.value];
layout.yaxis!.autorange = false;
}

newPlot(el as HTMLElement, baseData.value, layout, configCopy);

if (!lockYAxis.value || toggleLogScale) {
updateAxesRange();
}

if (props.recalculateOnRelayout) {
(el as EventEmitter).on("plotly_relayout", relayout);
}
Expand All @@ -123,7 +147,8 @@ export default defineComponent({

onMounted(drawPlot);

watch([() => props.redrawWatches, yAxisType], drawPlot);
watch([() => props.redrawWatches, lockYAxis], () => drawPlot());
watch(yAxisType, () => drawPlot(true));

onUnmounted(() => {
if (resizeObserver) {
Expand Down
20 changes: 19 additions & 1 deletion app/static/src/app/components/options/GraphSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
<input type="checkbox" class="form-check-input" style="vertical-align:bottom;" v-model="logScaleYAxis">
</div>
</div>
<div id="lock-y-axis" class="row my-2">
<div class="col-5">
<label class="col-form-label">Lock y axis</label>
</div>
<div class="col-6">
<input type="checkbox" class="form-check-input" style="vertical-align:bottom;" v-model="lockYAxis">
</div>
</div>
</div>
</template>

Expand All @@ -29,8 +37,18 @@ export default defineComponent({
}
});

const lockYAxis = computed({
get() {
return store.state.graphSettings.lockYAxis;
},
set(newValue) {
store.commit(`graphSettings/${GraphSettingsMutation.SetLockYAxis}`, newValue);
}
});

return {
logScaleYAxis
logScaleYAxis,
lockYAxis
};
}
});
Expand Down
4 changes: 3 additions & 1 deletion app/static/src/app/store/graphSettings/graphSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { GraphSettingsState } from "./state";
import { mutations } from "./mutations";

export const defaultState: GraphSettingsState = {
logScaleYAxis: false
logScaleYAxis: false,
lockYAxis: false,
yAxisRange: [0, 0]
};

export const graphSettings = {
Expand Down
14 changes: 12 additions & 2 deletions app/static/src/app/store/graphSettings/mutations.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { MutationTree } from "vuex";
import { GraphSettingsState } from "./state";
import { YAxisRange, GraphSettingsState } from "./state";

export enum GraphSettingsMutation {
SetLogScaleYAxis = "SetLogScaleYAxis"
SetLogScaleYAxis = "SetLogScaleYAxis",
SetLockYAxis = "SetLockYAxis",
SetYAxisRange = "SetYAxisRange"
}

export const mutations: MutationTree<GraphSettingsState> = {
[GraphSettingsMutation.SetLogScaleYAxis](state: GraphSettingsState, payload: boolean) {
state.logScaleYAxis = payload;
},

[GraphSettingsMutation.SetLockYAxis](state: GraphSettingsState, payload: boolean) {
state.lockYAxis = payload;
},

[GraphSettingsMutation.SetYAxisRange](state: GraphSettingsState, payload: YAxisRange) {
state.yAxisRange = payload;
}
};
6 changes: 5 additions & 1 deletion app/static/src/app/store/graphSettings/state.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export type YAxisRange = [number, number]

export interface GraphSettingsState {
logScaleYAxis: boolean
logScaleYAxis: boolean,
lockYAxis: boolean,
yAxisRange: YAxisRange
}
33 changes: 33 additions & 0 deletions app/static/tests/e2e/options.etest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,39 @@ test.describe("Options Tab tests", () => {
await expect(await page.innerHTML(tickSelector)).toBe("0.2M");
});

test("can change graph setting for lock axes", async ({ page }) => {
await expect(await page.innerText(":nth-match(.collapse-title, 3)")).toContain("Graph Settings");
await page.locator("#lock-y-axis input").click();

const tickSelector = ":nth-match(.plotly .ytick text, 6)";
await expect(await page.innerHTML(tickSelector)).toBe("1M");

await page.locator(":nth-match(.parameter-input, 3)").fill("1000000000");

await page.locator("#run-btn").click();

// would be 1B if we didn't lock the axes
await expect(await page.innerHTML(tickSelector)).toBe("1M");
await page.locator("#lock-y-axis input").click();

// autorange on deselect of lock axes
await expect(await page.innerHTML(tickSelector)).toBe("1B");
});

test("overrides axes lock if log scale toggle changes", async ({ page }) => {
await expect(await page.innerText(":nth-match(.collapse-title, 3)")).toContain("Graph Settings");
await page.locator("#lock-y-axis input").click();

const tickSelector = ":nth-match(.plotly .ytick text, 2)";
await expect(await page.innerHTML(tickSelector)).toBe("0.2M");

await page.locator("#log-scale-y-axis input").click();

// if you've locked the axis, it should not update to 10n, would
// be 10^115441
await expect(await page.innerHTML(tickSelector)).toBe("10n");
});

const createParameterSet = async (page: Page) => {
await page.click("#create-param-set");
};
Expand Down
2 changes: 2 additions & 0 deletions app/static/tests/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export const mockVersionsState = (states: Partial<VersionsState> = {}): Versions
export const mockGraphSettingsState = (state: Partial<GraphSettingsState> = {}): GraphSettingsState => {
return {
logScaleYAxis: false,
lockYAxis: false,
yAxisRange: [0, 0],
...state
};
};
Expand Down
36 changes: 28 additions & 8 deletions app/static/tests/unit/components/options/graphSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ import { GraphSettingsMutation } from "../../../../src/app/store/graphSettings/m

describe("GraphSettings", () => {
const mockSetLogScaleYAxis = jest.fn();
const mockSetLockYAxis = jest.fn();

const getWrapper = (logScaleYAxis = true) => {
const getWrapper = (logScaleYAxis = true, lockYAxis = true) => {
const store = new Vuex.Store<BasicState>({
modules: {
graphSettings: {
namespaced: true,
state: {
logScaleYAxis
logScaleYAxis,
lockYAxis
} as any,
mutations: {
[GraphSettingsMutation.SetLogScaleYAxis]: mockSetLogScaleYAxis
[GraphSettingsMutation.SetLogScaleYAxis]: mockSetLogScaleYAxis,
[GraphSettingsMutation.SetLockYAxis]: mockSetLockYAxis
}
}
}
Expand All @@ -34,16 +37,33 @@ describe("GraphSettings", () => {

it("renders as expected", () => {
const wrapper = getWrapper();
expect(wrapper.find("label").text()).toBe("Log scale y axis");
expect(wrapper.find("input").attributes("type")).toBe("checkbox");
expect((wrapper.find("input").element as HTMLInputElement).checked).toBe(true);
const labels = wrapper.findAll("label");
const inputs = wrapper.findAll("input");
expect(labels.length).toBe(2);
expect(inputs.length).toBe(2);
expect(labels[0].text()).toBe("Log scale y axis");
expect(inputs[0].attributes("type")).toBe("checkbox");
expect((inputs[0].element as HTMLInputElement).checked).toBe(true);
expect(labels[1].text()).toBe("Lock y axis");
expect(inputs[1].attributes("type")).toBe("checkbox");
expect((inputs[1].element as HTMLInputElement).checked).toBe(true);
});

it("commits change to log scale y axis setting", async () => {
const wrapper = getWrapper(false);
expect((wrapper.find("input").element as HTMLInputElement).checked).toBe(false);
await wrapper.find("input").setValue(true);
const inputs = wrapper.findAll("input");
expect((inputs[0].element as HTMLInputElement).checked).toBe(false);
await inputs[0].setValue(true);
expect(mockSetLogScaleYAxis).toHaveBeenCalledTimes(1);
expect(mockSetLogScaleYAxis.mock.calls[0][1]).toBe(true);
});

it("commits change to log scale y axis setting", async () => {
const wrapper = getWrapper(false, false);
const inputs = wrapper.findAll("input");
expect((inputs[1].element as HTMLInputElement).checked).toBe(false);
await inputs[1].setValue(true);
expect(mockSetLockYAxis).toHaveBeenCalledTimes(1);
expect(mockSetLockYAxis.mock.calls[0][1]).toBe(true);
});
});
63 changes: 58 additions & 5 deletions app/static/tests/unit/components/wodinPlot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Vuex, { Store } from "vuex";
import WodinPlot from "../../../src/app/components/WodinPlot.vue";
import WodinPlotDataSummary from "../../../src/app/components/WodinPlotDataSummary.vue";
import { BasicState } from "../../../src/app/store/basic/state";
import { GraphSettingsMutation } from "../../../src/app/store/graphSettings/mutations";

describe("WodinPlot", () => {
const mockPlotlyNewPlot = jest.spyOn(plotly, "newPlot");
Expand Down Expand Up @@ -58,34 +59,57 @@ describe("WodinPlot", () => {
placeholderMessage: "No data available"
};

const getStore = (graphSettingsLogScaleYAxis = false) => {
const mockSetYAxisRange = jest.fn();

const getStore = (logScaleYAxis = false, lockYAxis = false) => {
return new Vuex.Store<BasicState>({
state: {
modules: {
graphSettings: {
logScaleYAxis: graphSettingsLogScaleYAxis
namespaced: true,
state: {
logScaleYAxis,
lockYAxis,
yAxisRange: [10, 20]
},
mutations: {
[GraphSettingsMutation.SetYAxisRange]: mockSetYAxisRange
}
}
} as any
}
});
};

const getWrapper = (props = {}, store: Store<BasicState> = getStore()) => {
const div = document.createElement("div");
div.id = "root";
document.body.appendChild(div);

return shallowMount(WodinPlot, {
props: { ...defaultProps, ...props },
global: {
plugins: [store]
}
},
attachTo: "#root"
});
};

const mockLayout = {
xaxis: { range: [3, 4] },
yaxis: { range: [1, 2] }
};

const mockPlotElementOn = (wrapper: VueWrapper<any>) => {
const divElement = wrapper.find("div.plot").element;
const mockOn = jest.fn();
(divElement as any).on = mockOn;
(divElement as any).layout = mockLayout;
return mockOn;
};

afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
mockSetYAxisRange.mockReset();
});

it("renders plot ref element", () => {
Expand Down Expand Up @@ -335,6 +359,35 @@ describe("WodinPlot", () => {
});
});

it("locks axes if graph setting is set", async () => {
const store = getStore(false, true);
const wrapper = getWrapper({}, store);
mockPlotElementOn(wrapper);

wrapper.setProps({ redrawWatches: [{} as any] });
await nextTick();
expect(mockPlotlyNewPlot.mock.calls[0][2]).toStrictEqual({
margin: { t: 25 },
xaxis: { title: "Time" },
yaxis: { type: "linear", autorange: false, range: [10, 20] }
});
});

it("commits SetYAxisRange on drawPlot", async () => {
const store = getStore(false, false);
const wrapper = getWrapper({}, store);
mockPlotElementOn(wrapper);

(wrapper.vm as any).plot = {
layout: mockLayout,
on: jest.fn()
};
await nextTick();
await wrapper.setProps({ redrawWatches: [{} as any] });
await nextTick();
expect(mockSetYAxisRange.mock.calls[0][1]).toStrictEqual([1, 2]);
});

it("relayout uses graph settings log scale y axis value", async () => {
const store = getStore(true);
const wrapper = getWrapper({}, store);
Expand Down
Loading
Loading