-
-
Notifications
You must be signed in to change notification settings - Fork 414
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 data-driven styling #161
Merged
orangemug
merged 10 commits into
maplibre:master
from
pjsier:feature/data-driven-styles
Oct 11, 2017
Merged
Changes from 9 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
e057fca
Add data-driven styling to editor
pjsier d731fb2
Fix scss linter errors
pjsier 9e52b0b
Remove default from data properties
pjsier bba7aa3
Merge branch 'master' into feature/data-driven-styles
pjsier 2ffb3e7
Re-add default field after style update
pjsier 1d29f67
Check for property-function support on data styles
pjsier 148f64c
Restrict data function types, reorder buttons
pjsier 4af7a71
Rename ZoomSpecField to FunctionSpecField
pjsier b429202
Fix default field bug
pjsier fa0067c
Update mapbox deps, clarify data prop scope
pjsier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,358 @@ | ||
import React from 'react' | ||
import Color from 'color' | ||
|
||
import Button from '../Button' | ||
import SpecField from './SpecField' | ||
import NumberInput from '../inputs/NumberInput' | ||
import StringInput from '../inputs/StringInput' | ||
import SelectInput from '../inputs/SelectInput' | ||
import DocLabel from './DocLabel' | ||
import InputBlock from '../inputs/InputBlock' | ||
|
||
import AddIcon from 'react-icons/lib/md/add-circle-outline' | ||
import DeleteIcon from 'react-icons/lib/md/delete' | ||
import FunctionIcon from 'react-icons/lib/md/functions' | ||
import MdInsertChart from 'react-icons/lib/md/insert-chart' | ||
|
||
import capitalize from 'lodash.capitalize' | ||
|
||
function isZoomField(value) { | ||
return typeof value === 'object' && value.stops && typeof value.property === 'undefined' | ||
} | ||
|
||
function isDataField(value) { | ||
return typeof value === 'object' && value.stops && typeof value.property !== 'undefined' | ||
} | ||
|
||
/** Supports displaying spec field for zoom function objects | ||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property | ||
*/ | ||
export default class FunctionSpecProperty extends React.Component { | ||
static propTypes = { | ||
onChange: React.PropTypes.func.isRequired, | ||
fieldName: React.PropTypes.string.isRequired, | ||
fieldSpec: React.PropTypes.object.isRequired, | ||
|
||
value: React.PropTypes.oneOfType([ | ||
React.PropTypes.object, | ||
React.PropTypes.string, | ||
React.PropTypes.number, | ||
React.PropTypes.bool, | ||
React.PropTypes.array | ||
]), | ||
} | ||
|
||
addStop() { | ||
const stops = this.props.value.stops.slice(0) | ||
const lastStop = stops[stops.length - 1] | ||
if (typeof lastStop[0] === "object") { | ||
stops.push([ | ||
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value}, | ||
lastStop[1] | ||
]) | ||
} | ||
else { | ||
stops.push([lastStop[0] + 1, lastStop[1]]) | ||
} | ||
|
||
const changedValue = { | ||
...this.props.value, | ||
stops: stops, | ||
} | ||
|
||
this.props.onChange(this.props.fieldName, changedValue) | ||
} | ||
|
||
deleteStop(stopIdx) { | ||
const stops = this.props.value.stops.slice(0) | ||
stops.splice(stopIdx, 1) | ||
|
||
let changedValue = { | ||
...this.props.value, | ||
stops: stops, | ||
} | ||
|
||
if(stops.length === 1) { | ||
changedValue = stops[0][1] | ||
} | ||
|
||
this.props.onChange(this.props.fieldName, changedValue) | ||
} | ||
|
||
makeZoomFunction() { | ||
const zoomFunc = { | ||
stops: [ | ||
[6, this.props.value], | ||
[10, this.props.value] | ||
] | ||
} | ||
this.props.onChange(this.props.fieldName, zoomFunc) | ||
} | ||
|
||
getDataFunctionTypes(functionType) { | ||
if (functionType === "interpolated") { | ||
return ["categorical", "interval", "exponential"] | ||
} | ||
else { | ||
return ["categorical", "interval"] | ||
} | ||
} | ||
|
||
makeDataFunction() { | ||
const dataFunc = { | ||
property: "", | ||
type: "categorical", | ||
stops: [ | ||
[{zoom: 6, value: 0}, this.props.value], | ||
[{zoom: 10, value: 0}, this.props.value] | ||
] | ||
} | ||
this.props.onChange(this.props.fieldName, dataFunc) | ||
} | ||
|
||
changeStop(changeIdx, stopData, value) { | ||
const stops = this.props.value.stops.slice(0) | ||
stops[changeIdx] = [stopData, value] | ||
const changedValue = { | ||
...this.props.value, | ||
stops: stops, | ||
} | ||
this.props.onChange(this.props.fieldName, changedValue) | ||
} | ||
|
||
changeDataProperty(propName, propVal) { | ||
if (propVal) { | ||
this.props.value[propName] = propVal | ||
} | ||
else { | ||
delete this.props.value[propName] | ||
} | ||
this.props.onChange(this.props.fieldName, this.props.value) | ||
} | ||
|
||
getDataInput(value, dataLevel, zoomLevel) { | ||
const dataProps = { | ||
label: "Data value", | ||
value: dataLevel, | ||
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value) | ||
} | ||
if (this.props.value.type === "categorical") { | ||
return <StringInput {...dataProps} /> | ||
} | ||
else { | ||
return <NumberInput {...dataProps} /> | ||
} | ||
} | ||
|
||
renderDataProperty() { | ||
const dataFields = this.props.value.stops.map((stop, idx) => { | ||
const zoomLevel = stop[0].zoom | ||
const dataLevel = stop[0].value | ||
const value = stop[1] | ||
const deleteStopBtn = <DeleteStopButton onClick={this.deleteStop.bind(this, idx)} /> | ||
return <InputBlock key={idx} action={deleteStopBtn}> | ||
<div className="maputnik-data-spec-property-stop-edit"> | ||
<NumberInput | ||
value={zoomLevel} | ||
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)} | ||
min={0} | ||
max={22} | ||
/> | ||
</div> | ||
<div className="maputnik-data-spec-property-stop-data"> | ||
{this.getDataInput(value, dataLevel, zoomLevel)} | ||
</div> | ||
<div className="maputnik-data-spec-property-stop-value"> | ||
<SpecField | ||
fieldName={this.props.fieldName} | ||
fieldSpec={this.props.fieldSpec} | ||
value={value} | ||
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)} | ||
/> | ||
</div> | ||
</InputBlock> | ||
}) | ||
|
||
return <div className="maputnik-data-spec-block"> | ||
<div className="maputnik-data-spec-property"> | ||
<InputBlock | ||
doc={this.props.fieldSpec.doc} | ||
label={labelFromFieldName(this.props.fieldName)} | ||
> | ||
<div className="maputnik-data-spec-property-group"> | ||
<DocLabel | ||
label="Property" | ||
doc={"Input a data property to base styles off of."} | ||
/> | ||
<div className="maputnik-data-spec-property-input"> | ||
<StringInput | ||
value={this.props.value.property} | ||
onChange={propVal => this.changeDataProperty("property", propVal)} | ||
/> | ||
</div> | ||
</div> | ||
<div className="maputnik-data-spec-property-group"> | ||
<DocLabel | ||
label="Type" | ||
doc={"Select a type of data scale (default is 'categorical')."} | ||
/> | ||
<div className="maputnik-data-spec-property-input"> | ||
<SelectInput | ||
value={this.props.value.type} | ||
onChange={propVal => this.changeDataProperty("type", propVal)} | ||
options={this.getDataFunctionTypes(this.props.fieldSpec.function)} | ||
/> | ||
</div> | ||
</div> | ||
<div className="maputnik-data-spec-property-group"> | ||
<DocLabel | ||
label="Default" | ||
doc={"Input a default value for data if not covered by the scales."} | ||
/> | ||
<div className="maputnik-data-spec-property-input"> | ||
<SpecField | ||
fieldName={this.props.fieldName} | ||
fieldSpec={this.props.fieldSpec} | ||
value={this.props.value.default} | ||
onChange={(_, propVal) => this.changeDataProperty("default", propVal)} | ||
/> | ||
</div> | ||
</div> | ||
</InputBlock> | ||
</div> | ||
{dataFields} | ||
<Button | ||
className="maputnik-add-stop" | ||
onClick={this.addStop.bind(this)} | ||
> | ||
Add stop | ||
</Button> | ||
</div> | ||
} | ||
|
||
renderZoomProperty() { | ||
const zoomFields = this.props.value.stops.map((stop, idx) => { | ||
const zoomLevel = stop[0] | ||
const value = stop[1] | ||
const deleteStopBtn= <DeleteStopButton onClick={this.deleteStop.bind(this, idx)} /> | ||
|
||
return <InputBlock | ||
key={zoomLevel} | ||
doc={this.props.fieldSpec.doc} | ||
label={labelFromFieldName(this.props.fieldName)} | ||
action={deleteStopBtn} | ||
> | ||
<div> | ||
<div className="maputnik-zoom-spec-property-stop-edit"> | ||
<NumberInput | ||
value={zoomLevel} | ||
onChange={changedStop => this.changeStop(idx, changedStop, value)} | ||
min={0} | ||
max={22} | ||
/> | ||
</div> | ||
<div className="maputnik-zoom-spec-property-stop-value"> | ||
<SpecField | ||
fieldName={this.props.fieldName} | ||
fieldSpec={this.props.fieldSpec} | ||
value={value} | ||
onChange={(_, newValue) => this.changeStop(idx, zoomLevel, newValue)} | ||
/> | ||
</div> | ||
</div> | ||
</InputBlock> | ||
}) | ||
|
||
return <div className="maputnik-zoom-spec-property"> | ||
{zoomFields} | ||
<Button | ||
className="maputnik-add-stop" | ||
onClick={this.addStop.bind(this)} | ||
> | ||
Add stop | ||
</Button> | ||
</div> | ||
} | ||
|
||
renderProperty() { | ||
const functionBtn = <MakeFunctionButtons | ||
fieldSpec={this.props.fieldSpec} | ||
onZoomClick={this.makeZoomFunction.bind(this)} | ||
onDataClick={this.makeDataFunction.bind(this)} | ||
/> | ||
return <InputBlock | ||
doc={this.props.fieldSpec.doc} | ||
label={labelFromFieldName(this.props.fieldName)} | ||
action={functionBtn} | ||
> | ||
<SpecField {...this.props} /> | ||
</InputBlock> | ||
} | ||
|
||
render() { | ||
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property" | ||
let specField | ||
if (isZoomField(this.props.value)) { | ||
specField = this.renderZoomProperty() | ||
} | ||
else if (isDataField(this.props.value)) { | ||
specField = this.renderDataProperty() | ||
} | ||
else { | ||
specField = this.renderProperty() | ||
} | ||
return <div className={propClass}> | ||
{specField} | ||
</div> | ||
} | ||
} | ||
|
||
function MakeFunctionButtons(props) { | ||
let makeZoomButton, makeDataButton | ||
if (props.fieldSpec['zoom-function']) { | ||
makeZoomButton = <Button | ||
className="maputnik-make-zoom-function" | ||
onClick={props.onZoomClick} | ||
> | ||
<DocLabel | ||
label={<FunctionIcon />} | ||
cursorTargetStyle={{ cursor: 'pointer' }} | ||
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."} | ||
/> | ||
</Button> | ||
|
||
if (props.fieldSpec['property-function'] && ['piecewise-constant', 'interpolated'].indexOf(props.fieldSpec['function']) !== -1) { | ||
makeDataButton = <Button | ||
className="maputnik-make-data-function" | ||
onClick={props.onDataClick} | ||
> | ||
<DocLabel | ||
label={<MdInsertChart />} | ||
cursorTargetStyle={{ cursor: 'pointer' }} | ||
doc={"Turn property into a data function to enable a map feature to change according to data properties and the map's zoom level."} | ||
/> | ||
</Button> | ||
} | ||
return <div>{makeDataButton}{makeZoomButton}</div> | ||
} | ||
else { | ||
return null | ||
} | ||
} | ||
|
||
function DeleteStopButton(props) { | ||
return <Button | ||
className="maputnik-delete-stop" | ||
onClick={props.onClick} | ||
> | ||
<DocLabel | ||
label={<DeleteIcon />} | ||
doc={"Remove zoom level stop."} | ||
/> | ||
</Button> | ||
} | ||
|
||
function labelFromFieldName(fieldName) { | ||
let label = fieldName.split('-').slice(1).join(' ') | ||
return capitalize(label) | ||
} |
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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pjsier I think you mean
zoomLevel
here,idx
is undefinedThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@orangemug actually, I think I was accidentally doing some fancy scoping, because it does need to access the
idx
of the parent to change the stop. About to update that by removing thegetDataInput
and moving that conditional into the loop so it's more explicit