Skip to content

Commit a3a9a81

Browse files
authored
implement categorical scalar quantities (#316)
* implement categorical scalar quantities * revert demo app verbosity
1 parent 477f442 commit a3a9a81

31 files changed

+641
-80
lines changed

examples/demo-app/demo_app.cpp

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,28 @@ void constructDemoCurveNetwork(std::string curveName, std::vector<glm::vec3> nod
5252

5353
{ // Add some node values
5454
std::vector<double> valX(nNodes);
55+
std::vector<double> valNodeCat(nNodes);
5556
std::vector<double> valXabs(nNodes);
5657
std::vector<std::array<double, 3>> randColor(nNodes);
5758
std::vector<glm::vec3> randVec(nNodes);
5859
for (size_t iN = 0; iN < nNodes; iN++) {
5960
valX[iN] = nodes[iN].x;
61+
valNodeCat[iN] = iN * 5 / nNodes;
6062
valXabs[iN] = std::fabs(nodes[iN].x);
6163
randColor[iN] = {{polyscope::randomUnit(), polyscope::randomUnit(), polyscope::randomUnit()}};
6264
randVec[iN] = glm::vec3{polyscope::randomUnit() - .5, polyscope::randomUnit() - .5, polyscope::randomUnit() - .5};
6365
}
6466
polyscope::getCurveNetwork(curveName)->addNodeScalarQuantity("nX", valX);
6567
polyscope::getCurveNetwork(curveName)->addNodeScalarQuantity("nXabs", valXabs);
68+
polyscope::getCurveNetwork(curveName)->addNodeScalarQuantity("node categorical", valNodeCat,
69+
polyscope::DataType::CATEGORICAL);
6670
polyscope::getCurveNetwork(curveName)->addNodeColorQuantity("nColor", randColor);
6771
polyscope::getCurveNetwork(curveName)->addNodeVectorQuantity("randVecN", randVec);
6872
}
6973

7074
{ // Add some edge values
7175
std::vector<double> edgeLen(nEdges);
76+
std::vector<double> valEdgeCat(nEdges);
7277
std::vector<std::array<double, 3>> randColor(nEdges);
7378
std::vector<glm::vec3> randVec(nEdges);
7479
for (size_t iE = 0; iE < nEdges; iE++) {
@@ -77,10 +82,13 @@ void constructDemoCurveNetwork(std::string curveName, std::vector<glm::vec3> nod
7782
size_t nB = std::get<1>(edge);
7883

7984
edgeLen[iE] = glm::length(nodes[nA] - nodes[nB]);
85+
valEdgeCat[iE] = iE * 5 / nEdges;
8086
randColor[iE] = {{polyscope::randomUnit(), polyscope::randomUnit(), polyscope::randomUnit()}};
8187
randVec[iE] = glm::vec3{polyscope::randomUnit() - .5, polyscope::randomUnit() - .5, polyscope::randomUnit() - .5};
8288
}
8389
polyscope::getCurveNetwork(curveName)->addEdgeScalarQuantity("edge len", edgeLen, polyscope::DataType::MAGNITUDE);
90+
polyscope::getCurveNetwork(curveName)->addEdgeScalarQuantity("edge categorical", valEdgeCat,
91+
polyscope::DataType::CATEGORICAL);
8492
polyscope::getCurveNetwork(curveName)->addEdgeColorQuantity("eColor", randColor);
8593
polyscope::getCurveNetwork(curveName)->addEdgeVectorQuantity("randVecE", randVec);
8694
}
@@ -104,6 +112,7 @@ void processFileOBJ(std::string filename) {
104112
auto psMesh = polyscope::registerSurfaceMesh(niceName, vertexPositionsGLM, faceIndices);
105113

106114
auto psSimpleMesh = polyscope::registerSimpleTriangleMesh(niceName, vertexPositionsGLM, faceIndices);
115+
psSimpleMesh->setEnabled(false);
107116

108117
// Useful data
109118
size_t nVertices = psMesh->nVertices();
@@ -114,12 +123,14 @@ void processFileOBJ(std::string filename) {
114123
std::vector<double> valY(nVertices);
115124
std::vector<double> valZ(nVertices);
116125
std::vector<double> valMag(nVertices);
126+
std::vector<double> valCat(nVertices);
117127
std::vector<std::array<double, 3>> randColor(nVertices);
118128
for (size_t iV = 0; iV < nVertices; iV++) {
119129
valX[iV] = vertexPositionsGLM[iV].x / 10000;
120130
valY[iV] = vertexPositionsGLM[iV].y;
121131
valZ[iV] = vertexPositionsGLM[iV].z;
122132
valMag[iV] = glm::length(vertexPositionsGLM[iV]);
133+
valCat[iV] = (int32_t)(iV * 7 / nVertices) - 2;
123134

124135
randColor[iV] = {{polyscope::randomUnit(), polyscope::randomUnit(), polyscope::randomUnit()}};
125136
}
@@ -129,6 +140,8 @@ void processFileOBJ(std::string filename) {
129140
polyscope::getSurfaceMesh(niceName)->addVertexColorQuantity("vColor", randColor);
130141
polyscope::getSurfaceMesh(niceName)->addVertexScalarQuantity("cY_sym", valY, polyscope::DataType::SYMMETRIC);
131142
polyscope::getSurfaceMesh(niceName)->addVertexScalarQuantity("cNorm", valMag, polyscope::DataType::MAGNITUDE);
143+
polyscope::getSurfaceMesh(niceName)->addVertexScalarQuantity("categorical vert", valCat,
144+
polyscope::DataType::CATEGORICAL);
132145

133146
polyscope::getSurfaceMesh(niceName)->addVertexDistanceQuantity("cY_dist", valY);
134147
polyscope::getSurfaceMesh(niceName)->addVertexSignedDistanceQuantity("cY_signeddist", valY);
@@ -137,6 +150,7 @@ void processFileOBJ(std::string filename) {
137150
// Add some face scalars
138151
std::vector<double> fArea(nFaces);
139152
std::vector<double> zero(nFaces);
153+
std::vector<double> fCat(nFaces);
140154
std::vector<std::array<double, 3>> fColor(nFaces);
141155
for (size_t iF = 0; iF < nFaces; iF++) {
142156
std::vector<size_t>& face = faceIndices[iF];
@@ -153,10 +167,13 @@ void processFileOBJ(std::string filename) {
153167

154168
zero[iF] = 0;
155169
fColor[iF] = {{polyscope::randomUnit(), polyscope::randomUnit(), polyscope::randomUnit()}};
170+
fCat[iF] = (int32_t)(iF * 25 / nFaces) - 12;
156171
}
157172
polyscope::getSurfaceMesh(niceName)->addFaceScalarQuantity("face area", fArea, polyscope::DataType::MAGNITUDE);
158173
polyscope::getSurfaceMesh(niceName)->addFaceScalarQuantity("zero", zero);
159174
polyscope::getSurfaceMesh(niceName)->addFaceColorQuantity("fColor", fColor);
175+
polyscope::getSurfaceMesh(niceName)->addFaceScalarQuantity("categorical face", fCat,
176+
polyscope::DataType::CATEGORICAL);
160177

161178

162179
// size_t nEdges = psMesh->nEdges();
@@ -165,15 +182,19 @@ void processFileOBJ(std::string filename) {
165182
std::vector<double> eLen;
166183
std::vector<double> heLen;
167184
std::vector<double> cAngle;
185+
std::vector<double> cID;
186+
std::vector<double> eCat;
187+
std::vector<double> heCat;
188+
std::vector<double> cCat;
168189
std::unordered_set<std::pair<size_t, size_t>, polyscope::hash_combine::hash<std::pair<size_t, size_t>>> seenEdges;
169190
std::vector<uint32_t> edgeOrdering;
170191
for (size_t iF = 0; iF < nFaces; iF++) {
171192
std::vector<size_t>& face = faceIndices[iF];
172193

173-
for (size_t iV = 0; iV < face.size(); iV++) {
174-
size_t i0 = face[iV];
175-
size_t i1 = face[(iV + 1) % face.size()];
176-
size_t im1 = face[(iV + face.size() - 1) % face.size()];
194+
for (size_t iC = 0; iC < face.size(); iC++) {
195+
size_t i0 = face[iC];
196+
size_t i1 = face[(iC + 1) % face.size()];
197+
size_t im1 = face[(iC + face.size() - 1) % face.size()];
177198
glm::vec3 p0 = vertexPositionsGLM[i0];
178199
glm::vec3 p1 = vertexPositionsGLM[i1];
179200
glm::vec3 pm1 = vertexPositionsGLM[im1];
@@ -188,18 +209,29 @@ void processFileOBJ(std::string filename) {
188209
auto p = std::make_pair(iMin, iMax);
189210
if (seenEdges.find(p) == seenEdges.end()) {
190211
eLen.push_back(len);
212+
eCat.push_back((iF + iC) % 5);
191213
edgeOrdering.push_back(edgeOrdering.size()); // totally coincidentally, this is the trivial ordering
192214
seenEdges.insert(p);
193215
}
194216
heLen.push_back(len);
195217
cAngle.push_back(angle);
218+
heCat.push_back((iF + iC) % 7);
219+
cCat.push_back(i0 % 12);
220+
cID.push_back(iC);
196221
}
197222
}
198223
size_t nEdges = edgeOrdering.size();
199224
polyscope::getSurfaceMesh(niceName)->setEdgePermutation(edgeOrdering);
200225
polyscope::getSurfaceMesh(niceName)->addEdgeScalarQuantity("edge length", eLen);
201226
polyscope::getSurfaceMesh(niceName)->addHalfedgeScalarQuantity("halfedge length", heLen);
202227
polyscope::getSurfaceMesh(niceName)->addCornerScalarQuantity("corner angle", cAngle);
228+
polyscope::getSurfaceMesh(niceName)->addCornerScalarQuantity("corner ID", cID);
229+
polyscope::getSurfaceMesh(niceName)->addEdgeScalarQuantity("categorical edge", eCat,
230+
polyscope::DataType::CATEGORICAL);
231+
polyscope::getSurfaceMesh(niceName)->addHalfedgeScalarQuantity("categorical halfedge", heCat,
232+
polyscope::DataType::CATEGORICAL);
233+
polyscope::getSurfaceMesh(niceName)->addCornerScalarQuantity("categorical corner", cCat,
234+
polyscope::DataType::CATEGORICAL);
203235

204236

205237
// Test error
@@ -673,13 +705,16 @@ void addDataToPointCloud(std::string pointCloudName, const std::vector<glm::vec3
673705
// Add some scalar quantities
674706
std::vector<double> xC(points.size());
675707
std::vector<std::array<double, 3>> randColor(points.size());
708+
std::vector<double> cat(points.size());
676709
for (size_t i = 0; i < points.size(); i++) {
677710
xC[i] = points[i].x;
678711
randColor[i] = {{polyscope::randomUnit(), polyscope::randomUnit(), polyscope::randomUnit()}};
712+
cat[i] = i * 12 / points.size();
679713
}
680714
polyscope::getPointCloud(pointCloudName)->addScalarQuantity("xC", xC);
681715
polyscope::getPointCloud(pointCloudName)->addColorQuantity("random color", randColor);
682716
polyscope::getPointCloud(pointCloudName)->addColorQuantity("random color2", randColor);
717+
polyscope::getPointCloud(pointCloudName)->addScalarQuantity("categorical", cat, polyscope::DataType::CATEGORICAL);
683718

684719

685720
// Add some vector quantities

include/polyscope/affine_remapper.ipp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ inline std::string defaultColorMap(DataType type) {
1515
return "coolwarm";
1616
case DataType::MAGNITUDE:
1717
return "blues";
18+
case DataType::CATEGORICAL:
19+
return "hsv";
1820
break;
1921
}
2022
return "viridis";

include/polyscope/histogram.h

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@
1010

1111
namespace polyscope {
1212

13-
// A histogram that shows up in ImGUI
13+
// A histogram that shows up in ImGUI window
14+
// ONEDAY: we could definitely make a better histogram widget for categorical data...
15+
1416
class Histogram {
1517
public:
16-
Histogram(); // must call buildHistogram() with data after
17-
Histogram(std::vector<float>& values); // internally calls buildHistogram()
18+
Histogram(); // must call buildHistogram() with data after
19+
Histogram(std::vector<float>& values, DataType datatype); // internally calls buildHistogram()
1820

1921
~Histogram();
2022

21-
void buildHistogram(const std::vector<float>& values);
23+
void buildHistogram(const std::vector<float>& values, DataType datatype);
2224
void updateColormap(const std::string& newColormap);
2325

2426
// Width = -1 means set automatically
@@ -33,6 +35,7 @@ class Histogram {
3335
void fillBuffers();
3436
size_t rawHistBinCount = 51;
3537

38+
DataType dataType = DataType::STANDARD;
3639
std::vector<float> rawHistCurveY;
3740
std::vector<std::array<float, 2>> rawHistCurveX;
3841
std::pair<double, double> dataRange;

include/polyscope/render/color_maps.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ bool buildColormapSelector(std::string& cm, std::string fieldname = "##colormap_
3232
// - rainbow (CM_RAINBOW)
3333
// - jet (CM_JET)
3434
// - turbo (CM_TURBO)
35+
// - hsv (CM_HSV)
3536
//
3637
// Cyclic:
3738
// - phase (CM_PHASE)

include/polyscope/render/colormap_defs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ extern const std::vector<glm::vec3> CM_JET;
2121
extern const std::vector<glm::vec3> CM_TURBO;
2222
extern const std::vector<glm::vec3> CM_REDS;
2323
extern const std::vector<glm::vec3> CM_PHASE;
24+
extern const std::vector<glm::vec3> CM_HSV;
2425

2526

2627
} // namespace render

include/polyscope/render/opengl/shaders/cylinder_shaders.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ extern const ShaderStageSpecification FLEX_CYLINDER_FRAG_SHADER;
1616
// Rules specific to cylinders
1717
extern const ShaderReplacementRule CYLINDER_PROPAGATE_VALUE;
1818
extern const ShaderReplacementRule CYLINDER_PROPAGATE_BLEND_VALUE;
19+
extern const ShaderReplacementRule CYLINDER_PROPAGATE_NEAREST_VALUE;
1920
extern const ShaderReplacementRule CYLINDER_PROPAGATE_COLOR;
2021
extern const ShaderReplacementRule CYLINDER_PROPAGATE_BLEND_COLOR;
2122
extern const ShaderReplacementRule CYLINDER_PROPAGATE_PICK;

include/polyscope/render/opengl/shaders/histogram_shaders.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace backend_openGL3 {
1111
// High level pipeline
1212
extern const ShaderStageSpecification HISTOGRAM_VERT_SHADER;
1313
extern const ShaderStageSpecification HISTOGRAM_FRAG_SHADER;
14+
extern const ShaderStageSpecification HISTOGRAM_CATEGORICAL_FRAG_SHADER;
1415

1516
// Rules
1617
// extern const ShaderReplacementRule RULE_NAME;

include/polyscope/render/opengl/shaders/rules.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ extern const ShaderReplacementRule SHADE_BASECOLOR; // constant from
2222
extern const ShaderReplacementRule SHADE_COLOR; // from shadeColor
2323
extern const ShaderReplacementRule SHADECOLOR_FROM_UNIFORM;
2424
extern const ShaderReplacementRule SHADE_COLORMAP_VALUE; // colormapped from shadeValue
25+
extern const ShaderReplacementRule SHADE_CATEGORICAL_COLORMAP; // use ints to sample distinct values from colormap
2526
extern const ShaderReplacementRule SHADE_COLORMAP_ANGULAR2; // colormapped from angle of shadeValue2
2627
extern const ShaderReplacementRule SHADE_GRID_VALUE2; // generate a two-color grid with lines from shadeValue2
2728
extern const ShaderReplacementRule SHADE_CHECKER_VALUE2; // generate a two-color checker from shadeValue2

include/polyscope/render/opengl/shaders/surface_mesh_shaders.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ extern const ShaderReplacementRule MESH_BACKFACE_DARKEN;
2626
extern const ShaderReplacementRule MESH_PROPAGATE_VALUE;
2727
extern const ShaderReplacementRule MESH_PROPAGATE_VALUEALPHA;
2828
extern const ShaderReplacementRule MESH_PROPAGATE_FLAT_VALUE;
29+
extern const ShaderReplacementRule MESH_PROPAGATE_VALUE_CORNER_NEAREST;
2930
extern const ShaderReplacementRule MESH_PROPAGATE_INT;
3031
extern const ShaderReplacementRule MESH_PROPAGATE_VALUE2;
3132
extern const ShaderReplacementRule MESH_PROPAGATE_TCOORD;

include/polyscope/scalar_quantity.ipp

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ ScalarQuantity<QuantityT>::ScalarQuantity(QuantityT& quantity_, const std::vecto
2121
{
2222
values.checkInvalidValues();
2323
hist.updateColormap(cMap.get());
24-
hist.buildHistogram(values.data);
24+
hist.buildHistogram(values.data, dataType);
25+
// TODO: I think we might be building the histogram ^^^ twice for many quantities
2526

2627
if (vizRangeMin.holdsDefaultValue()) { // min and max should always have same cache state
2728
// dynamically compute a viz range from the data min/max
@@ -59,16 +60,23 @@ void ScalarQuantity<QuantityT>::buildScalarUI() {
5960
extraText = "This quantity was added as **magnitude** scalar quantity, so only a "
6061
"single symmetric range control can be adjusted, and it must be positive.";
6162
} break;
63+
case DataType::CATEGORICAL: {
64+
extraText = "This quantity was added as **categorical** scalar quantity, it is "
65+
"interpreted as integer labels, each shaded with a distinct color. "
66+
"Range controls are not used, vminmax are used only to set histogram limits, "
67+
"if provided.";
68+
} break;
69+
}
70+
std::string mainText = "The window below shows the colormap used to visualize this scalar, "
71+
"and a histogram of the the data values. The text boxes below show the "
72+
"range limits for the color map.\n\n";
73+
if (dataType != DataType::CATEGORICAL) {
74+
mainText += "To adjust the limit range for the color map, click-and-drag on the text "
75+
"box. Control-click to type a value, even one outside the visible range.";
6276
}
77+
mainText += extraText;
6378
ImGui::SameLine();
64-
ImGuiHelperMarker(("The window below shows the colormap used to visualize this scalar, "
65-
"and a histogram of the the data values. The text boxes below show the "
66-
"range limits for the color map."
67-
"\n\n"
68-
"To adjust the limit range for the color map, click-and-drag on the text "
69-
"box. Control-click to type a value, even one outside the visible range." +
70-
extraText)
71-
.c_str());
79+
ImGuiHelperMarker(mainText.c_str());
7280

7381

7482
// Draw the histogram of values
@@ -82,7 +90,7 @@ void ScalarQuantity<QuantityT>::buildScalarUI() {
8290
// valid reasons) links the resolution of the slider to the decimal width of the formatted number. When %g formats a
8391
// number with few decimal places, sliders can break. There is no way to set a minimum number of decimal places with
8492
// %g, unfortunately.
85-
{
93+
if (dataType != DataType::CATEGORICAL) {
8694

8795
float imPad = ImGui::GetStyle().ItemSpacing.x;
8896
ImGui::PushItemWidth((histWidth - imPad) / 2);
@@ -92,11 +100,11 @@ void ScalarQuantity<QuantityT>::buildScalarUI() {
92100
switch (dataType) {
93101
case DataType::STANDARD: {
94102

95-
changed = changed || ImGui::DragFloat("##min", &vizRangeMin.get(), speed, dataRange.first, vizRangeMax.get(),
96-
"%.5g", ImGuiSliderFlags_NoRoundToFormat);
103+
changed = changed | ImGui::DragFloat("##min", &vizRangeMin.get(), speed, dataRange.first, vizRangeMax.get(),
104+
"%.5g", ImGuiSliderFlags_NoRoundToFormat);
97105
ImGui::SameLine();
98-
changed = changed || ImGui::DragFloat("##max", &vizRangeMax.get(), speed, vizRangeMin.get(), dataRange.second,
99-
"%.5g", ImGuiSliderFlags_NoRoundToFormat);
106+
changed = changed | ImGui::DragFloat("##max", &vizRangeMax.get(), speed, vizRangeMin.get(), dataRange.second,
107+
"%.5g", ImGuiSliderFlags_NoRoundToFormat);
100108

101109
} break;
102110
case DataType::SYMMETRIC: {
@@ -116,10 +124,13 @@ void ScalarQuantity<QuantityT>::buildScalarUI() {
116124

117125
} break;
118126
case DataType::MAGNITUDE: {
119-
changed = changed || ImGui::DragFloat("##max", &vizRangeMax.get(), speed, 0.f, dataRange.second, "%.5g",
120-
ImGuiSliderFlags_NoRoundToFormat);
127+
changed = changed | ImGui::DragFloat("##max", &vizRangeMax.get(), speed, 0.f, dataRange.second, "%.5g",
128+
ImGuiSliderFlags_NoRoundToFormat);
121129

122130
} break;
131+
case DataType::CATEGORICAL: {
132+
// unused
133+
} break;
123134
}
124135

125136
if (changed) {
@@ -168,12 +179,19 @@ void ScalarQuantity<QuantityT>::buildScalarUI() {
168179
template <typename QuantityT>
169180
void ScalarQuantity<QuantityT>::buildScalarOptionsUI() {
170181
if (ImGui::MenuItem("Reset colormap range")) resetMapRange();
171-
if (ImGui::MenuItem("Enable isolines", NULL, isolinesEnabled.get())) setIsolinesEnabled(!isolinesEnabled.get());
182+
if (dataType != DataType::CATEGORICAL) {
183+
if (ImGui::MenuItem("Enable isolines", NULL, isolinesEnabled.get())) setIsolinesEnabled(!isolinesEnabled.get());
184+
}
172185
}
173186

174187
template <typename QuantityT>
175188
std::vector<std::string> ScalarQuantity<QuantityT>::addScalarRules(std::vector<std::string> rules) {
176-
rules.push_back("SHADE_COLORMAP_VALUE");
189+
if (dataType == DataType::CATEGORICAL) {
190+
rules.push_back("SHADE_CATEGORICAL_COLORMAP");
191+
} else {
192+
// common case
193+
rules.push_back("SHADE_COLORMAP_VALUE");
194+
}
177195
if (isolinesEnabled.get()) {
178196
rules.push_back("ISOLINE_STRIPE_VALUECOLOR");
179197
}
@@ -183,8 +201,10 @@ std::vector<std::string> ScalarQuantity<QuantityT>::addScalarRules(std::vector<s
183201

184202
template <typename QuantityT>
185203
void ScalarQuantity<QuantityT>::setScalarUniforms(render::ShaderProgram& p) {
186-
p.setUniform("u_rangeLow", vizRangeMin.get());
187-
p.setUniform("u_rangeHigh", vizRangeMax.get());
204+
if (dataType != DataType::CATEGORICAL) {
205+
p.setUniform("u_rangeLow", vizRangeMin.get());
206+
p.setUniform("u_rangeHigh", vizRangeMax.get());
207+
}
188208

189209
if (isolinesEnabled.get()) {
190210
p.setUniform("u_modLen", getIsolineWidth());
@@ -196,6 +216,7 @@ template <typename QuantityT>
196216
QuantityT* ScalarQuantity<QuantityT>::resetMapRange() {
197217
switch (dataType) {
198218
case DataType::STANDARD:
219+
case DataType::CATEGORICAL:
199220
vizRangeMin = dataRange.first;
200221
vizRangeMax = dataRange.second;
201222
break;
@@ -285,6 +306,9 @@ double ScalarQuantity<QuantityT>::getIsolineDarkness() {
285306

286307
template <typename QuantityT>
287308
QuantityT* ScalarQuantity<QuantityT>::setIsolinesEnabled(bool newEnabled) {
309+
if (dataType == DataType::CATEGORICAL) {
310+
newEnabled = false; // no isolines allowed for categorical
311+
}
288312
isolinesEnabled = newEnabled;
289313
quantity.refresh();
290314
requestRedraw();

0 commit comments

Comments
 (0)