Skip to content

Commit a2de4e7

Browse files
Merge pull request #10 from mindthemath/bugfix/axis-ticks
axes label fixes
2 parents 8f7eb7c + 52e064e commit a2de4e7

9 files changed

Lines changed: 315 additions & 38 deletions

File tree

hiplot/fetchers_demo.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,29 @@ def demo_big_floats() -> hip.Experiment:
375375
)
376376

377377

378+
def demo_decay() -> hip.Experiment:
379+
"""Simulated exponential decay: remaining = initial_mass * exp(-ln2 / half_life * elapsed).
380+
Produces a wide numeric range in remaining_grams (spans many orders of magnitude),
381+
useful for testing log-scale tick labels alongside plain numeric columns.
382+
"""
383+
rng = random.Random(42)
384+
data: t.List[t.Dict[str, t.Any]] = []
385+
for _ in range(500):
386+
initial_mass = rng.uniform(50, 500)
387+
half_life = rng.uniform(1, 30)
388+
elapsed = rng.uniform(1, 50)
389+
remaining = initial_mass * math.exp(-math.log(2) / half_life * elapsed)
390+
data.append({
391+
'initial_mass': initial_mass,
392+
'half_life': half_life,
393+
'elapsed_years': elapsed,
394+
'remaining_grams': remaining,
395+
})
396+
xp = hip.Experiment.from_iterable(data)
397+
xp.parameters_definition["remaining_grams"].type = hip.ValueType.NUMERIC_LOG
398+
return xp
399+
400+
378401
README_DEMOS: t.Dict[str, t.Callable[[], hip.Experiment]] = {
379402
"demo": demo,
380403
"demo_3xcols": demo_3xcols,
@@ -404,4 +427,5 @@ def demo_big_floats() -> hip.Experiment:
404427
"demo_col_html": demo_col_html,
405428
"demo_disable_table": demo_disable_table,
406429
"demo_big_floats": demo_big_floats,
430+
"demo_decay": demo_decay,
407431
}

hiplot/templates/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
return;
2323
}
2424
var options = {};
25+
var _sp = new URLSearchParams(location.search);
26+
var _provider = _sp.get("provider");
27+
if (_provider) {
28+
options.dataProviderName = _provider;
29+
}
2530
/*ON_LOAD_SCRIPT_INJECT*/
2631
var hiplot_instance = hiplot.render(document.getElementById("hiplot_element_id"), options);
2732
Object.assign(window, { hiplot_last_instance: hiplot_instance }); // For debugging

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "hiplot-mm"
7-
version = "0.0.4rc15"
7+
version = "0.0.4rc16"
88
description = "High dimensional Interactive Plotting tool"
99
readme = "README.md"
1010
license = "MIT"

src/distribution/plot.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import React from "react";
99
import * as d3 from "d3";
1010
import style from "../hiplot.module.css";
1111
import { create_d3_scale_without_outliers, ParamDef } from "../infertypes";
12-
import { convert_to_categorical_input } from "../lib/d3_scales";
12+
import { convert_to_categorical_input, getLogScaleTickValues } from "../lib/d3_scales";
1313
import { ParamType, Datapoint } from "../types";
1414

1515
function labelTextFromHtml(html: string, fallback: string): string {
@@ -85,12 +85,25 @@ export class DistributionPlot extends React.Component<DistributionPlotData, {}>
8585
figureHeight() {
8686
return this.props.height - margin.top - margin.bottom;
8787
}
88+
applyLogScaleTicks(axis: any, tickCount: number): void {
89+
if (this.props.param_def.type !== ParamType.NUMERICLOG) {
90+
return;
91+
}
92+
const tickValues = getLogScaleTickValues(this.dataScale.domain(), tickCount);
93+
if (tickValues) {
94+
axis.tickValues(tickValues);
95+
}
96+
if (this.props.param_def.ticks_format) {
97+
axis.tickFormat(d3.format(this.props.param_def.ticks_format));
98+
}
99+
}
88100
createDataAxis(dataScale: any, animate: boolean): void {
89101
if (this.isVertical()) {
90102
dataScale.range([0, this.figureWidth()]);
91-
d3.select(this.axisBottom.current).call(
92-
d3.axisBottom(dataScale).ticks(1 + this.props.width / 50),
93-
);
103+
const tickCount = 1 + this.props.width / 50;
104+
const axis = d3.axisBottom(dataScale).ticks(tickCount);
105+
this.applyLogScaleTicks(axis, tickCount);
106+
d3.select(this.axisBottom.current).call(axis);
94107
d3.select(this.axisBottomName.current)
95108
.html(null)
96109
.append("text")
@@ -101,15 +114,20 @@ export class DistributionPlot extends React.Component<DistributionPlotData, {}>
101114
this.axisRight.current.innerHTML = "";
102115
} else {
103116
dataScale.range([this.figureHeight(), 0]);
117+
const tickCount = 1 + this.props.height / 50;
118+
const axisRight = d3.axisRight(dataScale).ticks(tickCount);
119+
const axisLeft = d3.axisLeft(dataScale).ticks(tickCount);
120+
this.applyLogScaleTicks(axisRight, tickCount);
121+
this.applyLogScaleTicks(axisLeft, tickCount);
104122
d3.select(this.axisRight.current)
105123
.transition()
106124
.duration(animate ? this.props.animateMs : 0)
107-
.call(d3.axisRight(dataScale).ticks(1 + this.props.height / 50))
125+
.call(axisRight)
108126
.attr("text-anchor", "end");
109127
d3.select(this.axisLeft.current)
110128
.transition()
111129
.duration(animate ? this.props.animateMs : 0)
112-
.call(d3.axisLeft(dataScale).ticks(1 + this.props.height / 50));
130+
.call(axisLeft);
113131
d3.select(this.axisBottomName.current)
114132
.html(null)
115133
.append("text")

src/lib/d3_scales.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,39 @@ export function d3_scale_percentile_values_sorted(values: Array<number>): d3Scal
242242
return scale;
243243
}
244244

245+
/**
246+
* Generate tick values restricted to powers of 10 for log scales.
247+
* Thins out ticks if there are more than fit in the available space.
248+
*/
249+
export function getLogScaleTickValues(domain: number[], approxTickCount: number): number[] | null {
250+
if (!Array.isArray(domain) || domain.length < 2) {
251+
return null;
252+
}
253+
const dMin = Number(domain[0]);
254+
const dMax = Number(domain[domain.length - 1]);
255+
if (!Number.isFinite(dMin) || !Number.isFinite(dMax) || dMin <= 0 || dMax <= 0) {
256+
return null;
257+
}
258+
// Use ceil/floor to keep ticks within the domain, preventing labels
259+
// from bleeding outside the chart area (especially on the distribution plot).
260+
const minExp = Math.ceil(Math.log10(dMin));
261+
const maxExp = Math.floor(Math.log10(dMax));
262+
const majorTicks: number[] = [];
263+
for (let exp = minExp; exp <= maxExp; exp++) {
264+
majorTicks.push(Math.pow(10, exp));
265+
}
266+
if (majorTicks.length > approxTickCount && majorTicks.length > 2) {
267+
const step = Math.ceil(majorTicks.length / approxTickCount);
268+
const thinned: number[] = [majorTicks[0]];
269+
for (let i = step; i < majorTicks.length - 1; i += step) {
270+
thinned.push(majorTicks[i]);
271+
}
272+
thinned.push(majorTicks[majorTicks.length - 1]);
273+
return thinned;
274+
}
275+
return majorTicks.length > 1 ? majorTicks : null;
276+
}
277+
245278
function cpy_properties(from, to) {
246279
for (var prop in from) {
247280
if (from.hasOwnProperty(prop)) {

src/parallel/parallel.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { HiPlotPluginData } from "../plugin";
2121
import { ResizableH } from "../lib/resizable";
2222
import { Filter, FilterType, apply_filters } from "../filters";
2323
import { IS_MOBILE } from "../lib/browsercompat";
24+
import { getLogScaleTickValues } from "../lib/d3_scales";
2425

2526
interface StringMapping<V> {
2627
[key: string]: V;
@@ -347,8 +348,28 @@ export class ParallelPlot extends React.Component<ParallelPlotData, ParallelPlot
347348
}
348349

349350
get_axis = function (d: string) {
350-
const fmt = this.props.params_def[d].ticks_format;
351-
var axis = this.axis.scale(this.yscale[d]).ticks(1 + this.state.height / 50, fmt);
351+
const pd = this.props.params_def[d];
352+
const fmt = pd.ticks_format;
353+
const tickCount = 1 + this.state.height / 50;
354+
var axis = this.axis.scale(this.yscale[d]).ticks(tickCount, fmt);
355+
if (pd.type === ParamType.NUMERICLOG) {
356+
const scale = this.yscale[d];
357+
const domain = scale.domain();
358+
const tickValues = getLogScaleTickValues(domain, tickCount);
359+
if (tickValues) {
360+
// Preserve the NaN tick if the scale includes outlier handling
361+
if (scale.__scale_orig) {
362+
tickValues.push(NaN);
363+
}
364+
axis.tickValues(tickValues);
365+
} else {
366+
// Clear any tickValues left from a previous column on the shared axis
367+
axis.tickValues(null);
368+
}
369+
} else {
370+
// Clear any tickValues left from a previous column on the shared axis
371+
axis.tickValues(null);
372+
}
352373
return axis;
353374
}.bind(this);
354375

src/plotxy.tsx

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import _ from "underscore";
1717
import { Datapoint, ParamType } from "./types";
1818
import { ContextMenu } from "./contextmenu";
1919
import { IS_MOBILE } from "./lib/browsercompat";
20+
import { getLogScaleTickValues } from "./lib/d3_scales";
2021

2122
// DISPLAYS_DATA_DOC_BEGIN
2223
// Corresponds to values in the dict of `exp.display_data(hip.Displays.XY)`
@@ -233,34 +234,7 @@ export class PlotXY extends React.Component<PlotXYProps, PlotXYState> {
233234
) {
234235
return null;
235236
}
236-
const domain = scale.domain();
237-
if (!Array.isArray(domain) || domain.length < 2) {
238-
return null;
239-
}
240-
const dMin = Number(domain[0]);
241-
const dMax = Number(domain[domain.length - 1]);
242-
if (!Number.isFinite(dMin) || !Number.isFinite(dMax) || dMin <= 0 || dMax <= 0) {
243-
return null;
244-
}
245-
// Generate powers of 10 spanning the domain, including boundary powers
246-
// so that e.g. domain [1.05, 99.3] produces ticks [1, 10, 100]
247-
const minExp = Math.floor(Math.log10(dMin));
248-
const maxExp = Math.ceil(Math.log10(dMax));
249-
const majorTicks: number[] = [];
250-
for (let exp = minExp; exp <= maxExp; exp++) {
251-
majorTicks.push(Math.pow(10, exp));
252-
}
253-
// Thin out if there are too many powers of 10 for the available space
254-
if (majorTicks.length > approxTickCount && majorTicks.length > 2) {
255-
const step = Math.ceil(majorTicks.length / approxTickCount);
256-
const thinned: number[] = [majorTicks[0]];
257-
for (let i = step; i < majorTicks.length - 1; i += step) {
258-
thinned.push(majorTicks[i]);
259-
}
260-
thinned.push(majorTicks[majorTicks.length - 1]);
261-
return thinned;
262-
}
263-
return majorTicks.length > 1 ? majorTicks : null;
237+
return getLogScaleTickValues(scale.domain(), approxTickCount);
264238
}
265239
function redraw_axis() {
266240
me.svg.selectAll(".axis_render").remove();

0 commit comments

Comments
 (0)